<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>نوشته های MohamadReza Salehi</title>
        <link>https://virgool.io/feed/@mr_salehi</link>
        <description></description>
        <language>fa</language>
        <pubDate>2026-07-06 11:04:35</pubDate>
        <image>
            <url>https://files.virgool.io/upload/users/4143464/avatar/l9DXci.jpg?height=120&amp;width=120</url>
            <title>MohamadReza Salehi</title>
            <link>https://virgool.io/@mr_salehi</link>
        </image>

                    <item>
                <title>همبستگی (Connascence) در معماری نرم افزار</title>
                <link>https://virgool.io/@mr_salehi/connascence-ysmirtjkomcb</link>
                <description>تو این مقاله میخوایم به معنی کانسنس و انواع اون در نرم افزار و کاربرد عملی اون برای ریفکتور کردن و طراحی بهتر نرم افزار ها میپردازیم. ( این مقاله از روی کتاب Fundamentals of Software Architecture نوشته شده است)تا حالا براتون پیش اومده یه تغییر خیلی کوچیک توی یک بخش از کد بدید و ناگهان ببینید چند تا ماژول یا سرویس دیگه بدون هیچ دلیل واضحی از کار افتادن؟ این همون کابوسیه که مهندس‌های نرم‌افزار بهش میگن «دردسر وابستگی» یا Coupling. اما توی دنیای معماری نرم‌افزار، یک کلمه دقیق‌تر و جذاب‌تر برای توصیف این دردسر وجود داره: Connascence (کانسنس).اگه به عنوان مدیر فنی درگیر بدهی‌های فنی پروژه‌اید، یا به عنوان یک مهندس نرم‌افزار می‌خواید سیستمی طراحی کنید که فردا روزی توسعه‌اش تبدیل به یک کار فرسایشی نشه، درک مفهوم کانسنس براتون از نون شب واجب‌تره.کانسنس اصلا یعنی چی؟این مفهوم رو اولین بار آقای میلی پیج-جونز (Meilir Page-Jones) مطرح کرد. تعریفش خیلی ساده، اما به شدت عمیقه:“دو تا کامپوننت زمانی با هم کانسنس دارن که اگه یکی‌شون رو تغییر دادی، مجبور بشی اون یکی رو هم تغییر بدی تا سیستم همچنان درست کار کنه.”هرچی کانسنس تو سیستم شما بیشتر باشه، یعنی سیستم شما شکننده‌تره. پیج-جونز کانسنس‌ها رو به دو دسته بزرگ تقسیم می‌کنه: استاتیک و داینامیک. بیاید خیلی شفاف و مستقیم بررسیشون کنیم.۱. کانسنس‌های استاتیک (Static Connascence)این نوع وابستگی‌ها تو همون سطح سورس‌کد اتفاق می‌افتن و معمولا با یه نگاه به کد (یا با کمک IDEها) می‌شه پیداشون کرد. ۵ نوع اصلی داره:کانسنس نام (Name): ساده‌ترین نوعشه. چند تا کامپوننت باید روی اسم یک متد یا کلاس توافق داشته باشن. اگه اسمش رو تو یکی عوض کنی، تو بقیه هم باید عوض بشه.کانسنس نوع داده (Type): کامپوننت‌ها باید روی تایپ یک متغیر توافق کنن. اگه یه متغیر از نوع String رو بکنی Int، هرجا که داره ازش استفاده می‌شه هم باید آپدیت بشه.کانسنس معنا یا قرارداد (Meaning/Convention): این همون تله‌ی “اعداد جادویی” (Magic Numbers) در کده. مثلاً تو دیتابیس هاردکد کردید که وضعیت 1 یعنی کاربر فعال. حالا همه کامپوننت‌ها باید این معنا رو بدونن و روش توافق داشته باشن!کانسنس موقعیت (Position): وابستگی به ترتیب و جایگاه. مثلاً یک فانکشن دارید که ۳ تا پارامتر می‌گیره. ترتیب پاس دادن این پارامترها به فانکشن مهمه و اگه عوض بشه، همه‌چیز به هم می‌ریزه.کانسنس الگوریتم (Algorithm): وقتی دو بخش از سیستم باید حتماً از یک الگوریتم مشترک استفاده کنن. مثال بارزش الگوریتم‌های هشینگ پسورد بین کلاینت و سروره. اگر سرور الگوریتمش رو عوض کنه، کلاینت هم حتماً باید خودش رو تغییر بده.۲. کانسنس‌های داینامیک (Dynamic Connascence)اینجاست که کار سخت می‌شه! این وابستگی‌ها تو زمان اجرا (Runtime) خودشون رو نشون می‌دن و پیدا کردنشون موقع کد زدن اصلاً راحت نیست:کانسنس اجرا (Execution): وقتی ترتیب اجرای کارها مهمه. مثلاً برای ارسال ایمیل، اول باید کانکشن ساخته بشه، بعد بدنه ایمیل ست بشه و در نهایت متد send() فراخوانی بشه. اگه این ترتیب به هم بخوره، سیستم ارور می‌ده.کانسنس زمان‌بندی (Timing): وقتی صحت اجرای سیستم به زمان وابسته است. مشکل معروف Race Condition دقیقاً همینجاست؛ وقتی دو تا ترد (Thread) همزمان می‌خوان یک دیتا رو تغییر بدن و زمان‌بندیِ اجرا، نتیجه نهایی رو عوض می‌کنه.کانسنس مقادیر (Values): وقتی چند تا مقدار مختلف توی سیستم باید حتماً با هم تغییر کنن. بهترین مثالش تراکنش‌های توزیع‌شده (Distributed Transactions) هستن؛ یا همه تغییرات باید با هم اعمال بشن، یا هیچ‌کدوم!کانسنس هویت (Identity): وقتی چند تا کامپوننت مختلف دارن دقیقاً به یک «موجودیت» (Instance) مشترک تو مموری اشاره می‌کنن و وابستگی شدیدی به اون دارن.ویژگی‌های کانسنس: چرا بعضی وابستگی‌ها سیستم را شکننده می‌کنند؟تا اینجا فهمیدیم که کانسنس چطور می‌تونه یک سیستم نرم‌افزاری رو به شدت شکننده کنه. اما نکته مهم اینجاست که کانسنس فقط یک «مفهوم توصیفی» نیست؛ یک ابزار تحلیلی و کاربردی برای تصمیم‌گیری‌های معماری هم هست.برای اینکه بتونیم کانسنس رو درست تحلیل و مدیریت کنیم، سه ویژگی کلیدی وجود داره که باید همیشه زیر نظر بگیریمشون:1. قدرت کانسنس (Strength)قدرت کانسنس مشخص می‌کنه که تغییر دادن یا ریفکتور کردن یک وابستگی چقدر سخت و پرریسکه. بعضی از کانسنس‌ها ذاتاً ضعیف‌تر و مدیریت‌پذیرترن، و بعضی‌ها هم به شدت قوی، خطرناک و پرهزینه.هرچی یک کانسنس قوی‌تر باشه:تغییر دادنش سخت‌ترهاثرش روی کل سیستم بیشترهاحتمال شکستن بخش‌های دیگر سیستم بالاترهیک اصل مهم اینه که تا جای ممکن سیستم رو باید به سمت کانسنس‌های ضعیف‌تر ببریم.مثال خیلی معروفش «اعداد جادویی» در کده:// روش ضعیف و مبهم
if (status == 1) // CoM

// روش بهتر و شفاف
if (status == ACTIVE_USER) // CoTاینجا از یک وابستگی مبهم به یک وابستگی شفاف منتقل شدیم.این دقیقاً همون حرکتیه که قدرت کانسنس رو کاهش می‌ده.یک نکته مهم‌تر:کانسنس استاتیک تقریباً همیشه بهتر از کانسنس داینامیکه، چون که کانسنس داینامیک فقط هنگام اجرا معلوم می‌شه؛ یعنی دیر متوجهش می‌شی، پیدا کردنش سخته، و دیباگش معمولاً یک کابوسه.2. فاصله یا مجاورت وابستگی‌ها (Locality)لوکالیتی یا فاصله کانسنس مشخص می‌کنه که اجزای وابسته چقدر به هم نزدیک یا دور هستن.همون نوع کانسنس، اگر در یک محدوده نزدیک اتفاق بیفته قابل تحمله٬ اما اگر بین بخش‌های دور از هم بوجود بیاد، خطرناک می‌شه.مثال:دو کلاس داخل یک ماژول که کانسنس Meaning دارن: قابل قبولهدو سرویس مستقل با همین کانسنس : خطرناکچرا خطرناک؟چون هر تغییری باید در چند نقطه مختلف اعمال بشه.هماهنگی سخت‌تره، احتمال ناسازگاری بالاتر می‌ره و سیستم شکننده‌تر می‌شه.پس همیشه باید قدرت و فاصله کانسنس رو با هم تحلیل کنیم:کانسنس قوی داخل یک ماژول: قابل مدیریتهمان کانسنس قوی بین دو سیستم مستقل: نشانه یک مشکل طراحی جدی3. گستردگی وابستگی‌ها (Degree)گستردگی کانسنس مشخص می‌کنه که چند بخش از سیستم تحت تأثیر یک وابستگی هستن.هرچی تعداد ماژول‌ها یا سرویس‌های درگیر بیشتر باشه، کنترل و نگهداری سخت‌تر و ریسک سیستم بالاتر می‌ره.مثال:کانسنس بین دو کلاس: قابل کنترلکانسنس بین ۲۰ سرویس: تقریباً غیرقابل مدیریتنکته مهم اینه که حتی یک کانسنس قوی داینامیک اگر فقط چند بخش کوچک رو درگیر کنه، ممکنه خطری نداشته باشه.اما مشکل اینجاست که سیستم‌ها در طول زمان رشد می‌کنن و چیزی که امروز کوچک و بی‌خطره، فردا می‌تونه تبدیل به یک بحران تمام‌عیار بشه.سه توصیه Page-Jones برای مدیریت کانسنس در معماریبرای اینکه کانسنس به جای تهدید، تبدیل به ابزار کمکی ما در طراحی بشه، این سه اصل کمکمون میکنه:1. کاهش کلی کانسنس با بخش‌بندی درست سیستم(encapsulate)هرچه سیستم رو به بخش‌های مستقل‌تر تقسیم کنیم، مرزها روشن‌تر می‌شن و وابستگی‌ها کاهش پیدا می‌کنن.این یعنی پیچیدگی کمتر، تغییرات ایزوله‌تر و سیستم قابل کنترل‌تر.2. ضعیف کردن وابستگی‌هایی که از مرزها عبور می‌کنناگر مجبوریم بین بخش‌ها ارتباط داشته باشیم، باید این ارتباط رو تا جای ممکن ضعیف و کنترل‌پذیر نگه داریم.مثال:به جای اشتراک مستقیم دیتا: استفاده از APIبه جای وابستگی‌های زمان اجرا: استفاده از Message Queue3. افزایش کانسنس درون مرزهای هر بخشداخل یک ماژول، کانسنس نه‌تنها بد نیست؛ بلکه مفید هم هست.چون تغییرات در یک نقطه متمرکز می‌شن، پیچیدگی پخش نمی‌شه و هماهنگی ساده‌تره.جمع بندیConnascence فقط یه واژه شیک تو معماری نرم‌افزار نیست؛ در واقع یه زبان دقیق برای حرف زدن درباره کیفیت وابستگی‌هاست. اگر coupling فقط می‌گه «بخش‌های سیستم چقدر به هم وصلن»، connascence می‌گه «این اتصال دقیقاً چطور کار می‌کنه و تغییر دادنش چه هزینه‌ای داره». همین تفاوت برای کسی که می‌خواد سیستم بسازه، ریفکتور کنه یا مقیاسش بده، خیلی مهمه.در نهایت هم معماری خوب از این به دست نمیاد که همه وابستگی‌ها رو حذف کنیم؛ چنین چیزی عملاً ممکن نیست. هنر کار اینه که نوع وابستگی‌ها رو درست انتخاب کنیم. همون جایی که سیستم ساده‌تر، قابل‌فهم‌تر و قابل نگهداری‌تر می‌شه؛ و دقیقاً همون نقطه‌ایه که یه مهندس نرم‌افزار کم‌کم از «کدنویس» به «معمار» نزدیک می‌شه.</description>
                <category>MohamadReza Salehi</category>
                <author>MohamadReza Salehi</author>
                <pubDate>Sun, 03 May 2026 18:01:42 +0330</pubDate>
            </item>
                    <item>
                <title>این Test Double چیست؟ و انواع تایپ آن</title>
                <link>https://virgool.io/@mr_salehi/what-is-test-double-y7boooicqics</link>
                <description>این مقاله به بررسی جامع و عمیق مفهوم Test Double در مهندسی نرم‌افزار می‌پردازد، ابزاری حیاتی که در قلب توسعه آزمون‌محور (TDD) قرار دارد و نقش بنیادینی در خلق کدهای پاک، قابل نگهداری و عاری از خطا ایفا می‌کند.Test Double: چرا و چگونه؟در قلب فرایند توسعه نرم‌افزار مدرن، یونیت تستینگ (Unit Testing) قرار دارد. هدف اصلی آن، ایزوله کردن یک واحد کوچک از کد (معمولاً یک متد یا کلاس) و تأیید رفتار صحیح آن است. با این حال، در دنیای واقعی، واحدهای کد اغلب با وابستگی‌های خارجی (Dependencies) در ارتباط هستند. این وابستگی‌ها می‌توانند شامل پایگاه داده، سرویس‌های تحت شبکه، فایل سیستم‌ها، یا حتی کلاس‌های پیچیده‌تر در خود سیستم باشند.استفاده از این وابستگی‌های واقعی در محیط تست، چندین چالش عمده ایجاد می‌کند که فرایند تست را کند، غیرقابل اعتماد و پر از عوارض جانبی ناخواسته می‌سازد. برای مثال:کندی: تعامل با یک پایگاه داده یا برقراری تماس‌های شبکه، زمان‌بر است. این امر، سرعت اجرای تست‌ها را به شدت کاهش می‌دهد و چرخه بازخورد (feedback loop) را طولانی می‌کند، که برای توسعه‌دهنده مخرب است.ناپایداری: سرویس‌های خارجی ممکن است در دسترس نباشند یا به دلایل مختلف (مانند قطعی شبکه یا تغییر در API) رفتار غیرقابل پیش‌بینی داشته باشند. این امر منجر به تست‌های شکننده (brittle) می‌شود که حتی بدون تغییر در کد مورد آزمایش، ممکن است با شکست مواجه شوند.عوارض جانبی: اجرای یک تست نباید منجر به ارسال ایمیل‌های واقعی، انجام تراکنش‌های بانکی، یا تغییر دائمی در داده‌های پایگاه داده شود. این عوارض، محیط تست را آلوده و مدیریت آن را دشوار می‌سازند.دسترسی زودهنگام: گاهی اوقات، وابستگی مورد نیاز هنوز در حال توسعه است و وجود خارجی ندارد.Test Double دقیقاً برای حل این مشکلات طراحی شده است. این اصطلاح، یک پوشش (umbrella term) برای هر شیئی است که در طول تست، جایگزین یک وابستگی واقعی می‌شود. هدف اصلی آن، جداسازی سیستم تحت تست (System Under Test - SUT) از وابستگی‌هایش است تا بتوان تنها رفتار داخلی SUT را با دقت و سرعت بالا مورد بالا مورد ارزیابی قرار داد.دسته‌بندی Test Double: از سادگی تا پیچیدگی رفتاریجرارد مساروس و مارتین فاولر، با تفکیک دقیق انواع Test Double، ابهامات موجود در این حوزه را از بین بردند. هر یک از این انواع، هدف و کاربرد مشخصی دارند که در ادامه به تفصیل بررسی می‌شوند.1. Dummy Object (شیء ساختگی)یک Dummy ساده‌ترین شکل Test Double است. این شیء تنها یک محل‌نگه‌دار (placeholder) است و هیچ منطقی را پیاده‌سازی نمی‌کند.توضیح دقیق: Dummy یک شیء یا یک مقدار است که به عنوان پارامتر به یک متد یا سازنده (constructor) پاس داده می‌شود، اما هرگز در طول اجرای تست از آن استفاده یا متدی از آن فراخوانی نمی‌شود. هدف اصلی آن، برآورده کردن امضای متد (method signature) و جلوگیری از خطای کامپایل است.رفتار: کاملاً منفعل (passive) و بی‌اثر است. این شیء هیچ state یا رفتاری ندارد و صرفاً برای پر کردن لیست آرگومان‌ها وجود دارد.مورد استفاده: در سناریوهایی که سیستم تحت تست (SUT) دارای وابستگی‌های متعددی است، اما منطق تستی که می‌نویسیم تنها به یکی از آن‌ها مربوط می‌شود. برای وابستگی‌های بی‌اهمیت، از Dummy استفاده می‌شود.مثال کد در PHP:فرض کنید یک کلاس OrderProcessor دارید که برای پردازش یک سفارش، نیاز به Order و User دارد، اما در یک تست خاص می‌خواهید فقط تأیید کنید که سفارش به درستی ذخیره می‌شود، بدون توجه به اینکه کدام کاربر آن را ثبت کرده است.// کلاس OrderProcessor
class OrderProcessor {

    public function process(Order $order, User $user) {

        // ...
        $order-&gt;save();
    }

}

class User {} // dummy object

// test
public function testOrderIsSaved() {

    $order = new Order();
    $dummyUser = new User(); // شیء ساختگی (Dummy)
    $processor = new OrderProcessor();

    $processor-&gt;process($order, $dummyUser);
}
2. Stub (پاسخ‌دهنده از پیش‌تعیین‌شده)Stubها Test Doubleهایی هستند که رفتاری از پیش‌برنامه‌ریزی‌شده و ثابت دارند.توضیح دقیق: Stub یک پیاده‌سازی ساده و کنترل‌شده از یک وابستگی است که مقادیر از پیش تعریف‌شده (canned answers) را در پاسخ به فراخوانی‌های متدهای خاص برمی‌گرداند. هدف اصلی آن، کنترل جریان داده به سیستم تحت تست (SUT) است. Stubها به ما اجازه می‌دهند تا ورودی‌های غیرمستقیم را برای SUT فراهم کنیم و وضعیت (state) آن را مورد آزمایش قرار دهیم.رفتار: Stubها هیچ منطق پیچیده‌ای ندارند. آن‌ها به سادگی یک مجموعه از پاسخ‌ها را بر اساس فراخوانی‌های مورد انتظار ذخیره می‌کنند و آن‌ها را در زمان مناسب برمی‌گردانند.مورد استفاده: زمانی که سیستم تحت تست، برای ادامه عملیات خود به داده‌هایی از یک وابستگی نیاز دارد. Stub به جای اتصال به وابستگی واقعی (مانند دیتابیس یا سرویس شبکه)، داده‌های مورد نیاز تست را به سرعت و به صورت قابل پیش‌بینی فراهم می‌کند. این نوع Test Double در تأیید وضعیت (State-based Verification) به شدت مورد استفاده قرار می‌گیرد.مثال کد در PHP:فرض کنید یک کلاس GradeCalculator دارید که برای محاسبه میانگین نمرات یک دانشجو، به یک سرویس GradebookService وابسته است.interface GradebookService {
    public function getGradesFor(int $studentId): array;
}

// کلاس مورد تست
class GradeCalculator {

    private GradebookService $gradebookService;

    public function __construct(GradebookService $gradebookService) {
        $this-&gt;gradebookService = $gradebookService;
    }

    public function calculateAverage(int $studentId): float {

        $grades = $this-&gt;gradebookService-&gt;getGradesFor($studentId);

        if (empty($grades)) {
            return 0.0;
        }

        return array_sum($grades) / count($grades);
    }
}

// پیاده‌سازی Stub
class GradebookServiceStub implements GradebookService {

    public function getGradesFor(int $studentId): array {

        // پاسخ از پیش تعیین شده
        if ($studentId === 123) {
            return [85, 90, 75];
        }

        return [];
    }
}

// test
public function testAverageCalculation() {

    $stub = new GradebookServiceStub();
    $calculator = new GradeCalculator($stub);

    $average = $calculator-&gt;calculateAverage(123);

    assertEquals(83.33, round($average, 2));
}
3. Fake Object (شیء جعلی)Fake یک پیاده‌سازی سبک‌وزن و جایگزین از یک وابستگی واقعی است که منطق داخلی خود را دارد.توضیح دقیق: Fakeها نسخه‌های ساده و بهینه‌سازی‌شده از وابستگی‌های پیچیده هستند. برخلاف Stub که صرفاً پاسخ‌های از پیش تعیین‌شده را برمی‌گرداند، یک Fake دارای یک پیاده‌سازی عملیاتی است که رفتار واقعی شیء اصلی را شبیه‌سازی می‌کند، اما به گونه‌ای که برای تست مناسب‌تر است (مثلاً سریع‌تر یا با عوارض جانبی کمتر). آن‌ها state داخلی خود را مدیریت می‌کنند و می‌توانند رفتارهای پیچیده‌تر را از خود نشان دهند.رفتار: Fakeها دارای منطق عملیاتی هستند. آن‌ها state خود را تغییر می‌دهند و فراخوانی‌ها را به صورت پویا پردازش می‌کنند. برای مثال، یک Fake Repository به جای اتصال به دیتابیس، داده‌ها را در یک آرایه درون حافظه ذخیره می‌کند.مورد استفاده: زمانی که جایگزین کردن یک وابستگی با یک نسخه سبک‌تر و سریع‌تر، مزایای قابل توجهی را برای تست‌ها فراهم می‌کند، مانند جایگزینی یک پایگاه داده سنگین با یک پایگاه داده درون حافظه.مثال کد در PHP:فرض کنید یک کلاس UserRepository دارید که وظیفه مدیریت کاربران را بر عهده دارد و به یک دیتابیس وابسته است.interface UserRepository {

    public function save(User $user): void;
    public function findById(int $id): ?User;

}


class InMemoryUserRepository implements UserRepository {

    private array $users = [];

    public function save(User $user): void {
        $this-&gt;users[$user-&gt;getId()] = $user;
    }

    public function findById(int $id): ?User {
        return $this-&gt;users[$id] ?? null;
    }
}


// کلاس مورد تست
class UserService {

    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository) {
        $this-&gt;userRepository = $userRepository;
    }

    public function registerNewUser(User $user): bool {

        if ($this-&gt;userRepository-&gt;findById($user-&gt;getId()) !== null) {
            return false;
        }

        $this-&gt;userRepository-&gt;save($user);
        return true;
    }
}

// test
public function testUserRegistration() {

    $fakeRepository = new InMemoryUserRepository();
    $userService = new UserService($fakeRepository);
    $newUser = new User(1, &#039;JohnDoe&#039;);

    assertTrue($userService-&gt;registerNewUser($newUser));
    assertNotNull($fakeRepository-&gt;findById(1));
}
4. Mock Object (شیء مقلد/ناظر)Mock یک Test Double است که برای تأیید رفتار (Behavior Verification) سیستم تحت تست طراحی شده است.توضیح دقیق: Mockها اشیائی هستند که با انتظارات از پیش برنامه‌ریزی شده‌اند. آن‌ها نه تنها پاسخ‌های از پیش تعیین‌شده (مانند Stub) را برمی‌گردانند، بلکه فراخوانی‌هایی که از سیستم تحت تست دریافت می‌کنند را نیز ثبت می‌کنند. پس از اجرای SUT، می‌توان Mock را مورد بررسی قرار داد تا مطمئن شد که SUT به درستی با آن تعامل کرده است.رفتار: Mockها برای تأیید اینکه متدهای خاصی با تعداد دفعات مشخص و با پارامترهای صحیح فراخوانی شده‌اند، مورد استفاده قرار می‌گیرند. آن‌ها معمولاً با فریم‌ورک‌های Mocking مانند Mockito در جاوا یا Mockery در PHP ساخته می‌شوند.مورد استفاده: زمانی که خروجی مستقیم یک متد مهم نیست، اما عوارض جانبی آن (یعنی فراخوانی متدهای دیگر روی وابستگی‌ها) اهمیت دارد. این امر در تست کردن متدهای از نوع Command (متدهایی که وضعیت سیستم را تغییر می‌دهند و مقداری برنمی‌گردانند) بسیار رایج است.مثال کد در PHP:فرض کنید یک کلاس OrderService دارید که پس از ثبت سفارش، یک ایمیل تأیید برای مشتری ارسال می‌کند.interface EmailService {

    public function sendEmail(string $to, string $subject, string $body):
    void;

}

// کلاس مورد تست
class OrderService {

    private EmailService $emailService;

    public function __construct(EmailService $emailService) {
        $this-&gt;emailService = $emailService;
    }

    public function placeOrder(Order $order, User $user): void {

        $order-&gt;setStatus(&#039;PLACED&#039;);
        $this-&gt;emailService-&gt;sendEmail(
            $user-&gt;getEmail(),
            &#039;Order Confirmation&#039;,
            &#039;Your order has been placed.&#039;
        );
    }
}

// test
use Mockery;

public function testOrderConfirmationEmailIsSent() {

    $mockEmailService = Mockery::mock(EmailService::class);

    $mockEmailService-&gt;shouldReceive(&#039;sendEmail&#039;)
                     -&gt;once()
                     -&gt;with(&#039;john@example.com&#039;, &#039;Order Confirmation&#039;, &#039;Your order has been placed.&#039;);

    $orderService = new OrderService($mockEmailService);
    $user = (object) [&#039;email&#039; =&gt; &#039;john@example.com&#039;];
    $order = new Order();

    $orderService-&gt;placeOrder($order, $user);

    Mockery::close();
}
5. Spy (جاسوس)Spy یک Test Double هیبریدی است که رفتار یک شیء واقعی را رصد می‌کند.توضیح دقیق: Spy به شما اجازه می‌دهد تا فراخوانی‌ها به یک شیء واقعی را رصد کنید، بدون اینکه رفتار اصلی آن را کاملاً جایگزین کنید. این نوع Test Double، شیء اصلی را &quot;دورپیچ&quot; (wrap) می‌کند و تمام فراخوانی‌های متد به آن را ثبت می‌کند. این کار به شما امکان می‌دهد تا متدهای خاصی را Stub کنید و همزمان، رفتار کلی شیء را تأیید کنید.رفتار: Spyها قابلیت‌های Stub و Mock را ترکیب می‌کنند. آن‌ها می‌توانند پاسخ‌های از پیش تعیین‌شده را برگردانند و همزمان، اطلاعاتی مانند تعداد دفعات فراخوانی و پارامترهای ارسالی را نیز ثبت کنند.مورد استفاده: زمانی که می‌خواهید یک متد خاص را Stub کنید و همزمان تأیید کنید که متدهای دیگر روی همان شیء، به درستی فراخوانی شده‌اند. این امر به ویژه زمانی مفید است که نمی‌خواهید کل شیء را Mock کنید، بلکه فقط به رصد یا تغییر رفتار یک متد خاص نیاز دارید.مثال کد در PHP:فرض کنید یک کلاس Mailer دارید که یک متد send برای ارسال ایمیل دارد. می‌خواهید در تست، تعداد دفعات فراخوانی این متد را بررسی کنید، بدون اینکه ایمیل واقعی ارسال شود.class Mailer {
    public function send(string $to, string $subject, string $body): bool {

        echo &quot;Sending email to {$to}\n&quot;;
        return true;
    }
}


// کلاس مورد تست
class NotificationService {
    private Mailer $mailer;

    public function __construct(Mailer $mailer) {
        $this-&gt;mailer = $mailer;
    }

    public function sendWelcomeEmail(string $email): bool {
        return $this-&gt;mailer-&gt;send($email, &#039;Welcome&#039;, &#039;Welcome to our service!&#039;);
    }
}

// test
public function testWelcomeEmailIsSent() {

    $realMailer = new Mailer();
    
    // create spy
    $spy = createSpyOn($realMailer); 

    $notificationService = new NotificationService($spy);

    $notificationService-&gt;sendWelcomeEmail(&#039;test@example.com&#039;);

    // تأیید رفتار
    assertCalledOnce($spy-&gt;send);
    assertCalledWith($spy-&gt;send, &#039;test@example.com&#039;, &#039;Welcome&#039;, &#039;Welcome to our service!&#039;);

}جمع‌بندی: Test Double، ستون فقرات کدنویسی پایداردرک و به‌کارگیری صحیح Test Double فراتر از یک تکنیک برنامه‌نویسی ساده است؛ این یک اصل بنیادین در توسعه نرم‌افزار مدرن است که به توسعه‌دهندگان قدرت و اعتماد به نفس لازم برای ایجاد کدهای تمیز، قابل نگهداری و باگ کمتر را می‌دهد. همانطور که یک بدلکار به کارگردان این امکان را می‌دهد که صحنه‌های پیچیده را بدون به خطر انداختن بازیگر اصلی ضبط کند، Test Double نیز به مهندسان نرم‌افزار اجازه می‌دهد تا منطق کسب‌وکار خود را در یک محیط کنترل‌شده و ایزوله آزمایش کنند.استفاده از Test Double در فرایند TDD یک نمونه بارز از این فلسفه است. با ایجاد Mockها و Stubها، توسعه‌دهندگان می‌توانند حتی قبل از آماده شدن وابستگی‌ها، تست‌های شکست‌خورده را بنویسند و به سرعت در چرخه &quot;قرمز-سبز-بازآرایی&quot; حرکت کنند. این رویکرد نه تنها سرعت توسعه را افزایش می‌دهد، بلکه با وادار کردن توسعه‌دهنده به طراحی کدهای با اتصال سست (loosely coupled)، به ایجاد معماری‌های ماژولار و قابل انعطاف کمک می‌کند. در نهایت، Test Double نه تنها یک ابزار برای تست، بلکه یک کاتالیزور برای بهبود کیفیت و پایداری کلی سیستم‌های نرم‌افزاری است.</description>
                <category>MohamadReza Salehi</category>
                <author>MohamadReza Salehi</author>
                <pubDate>Mon, 22 Sep 2025 14:28:12 +0330</pubDate>
            </item>
                    <item>
                <title>طراحی و پیاده‌سازی سرویس‌های حرفه‌ای در Object-Oriented Programming</title>
                <link>https://virgool.io/@mr_salehi/%D8%B7%D8%B1%D8%A7%D8%AD%DB%8C-%D9%88-%D9%BE%DB%8C%D8%A7%D8%AF%D9%87-%D8%B3%D8%A7%D8%B2%DB%8C-%D8%B3%D8%B1%D9%88%DB%8C%D8%B3-%D9%87%D8%A7%DB%8C-%D8%AD%D8%B1%D9%81%D9%87-%D8%A7%DB%8C-%D8%AF%D8%B1-object-oriented-programming-hajwntb4vurk</link>
                <description>Object-Oriented Programming تنها ایجاد کلاسی با چند متد و property نیست. این پارادایم برنامه‌نویسی دارای عمق و پیچیدگی‌هایی است که درک صحیح آن می‌تواند تفاوت چشمگیری در کیفیت نرم‌افزارهای تولیدی ایجاد کند. یکی از مهم‌ترین جنبه‌های OOP، طراحی و پیاده‌سازی service object ها است که نقش محوری در معماری نرم‌افزارهای مدرن ایفا می‌کنند.این مقاله بر اساس فصل دوم کتاب Object Design Style Guide نوشته Matthias Noback تهیه شده است و به بررسی عمیق تکنیک‌های پیاده‌سازی سرویس‌ها در OOP می‌پردازد. متأسفانه بسیاری از توسعه‌دهندگان، حتی با سال‌ها تجربه، در زمینه طراحی و پیاده‌سازی صحیح OOP دچار نواقص عمده‌ای هستند و اصول best practice را رعایت نمی‌کنند. این امر در آینده منجر به بحران‌ها و مشکلات جدی برای توسعه و نگهداری نرم‌افزار خواهد شد.در این مقاله به service object می‌پردازیم: تعریف آن، اصول طراحی، نحوه ارتباط بین سرویس‌ها، روش‌های تزریق وابستگی‌ها و نکات کلیدی دیگر در مورد سرویس‌ها. (تمامی نکات ارائه شده خلاصه‌ای از فصل مذکور است و تمامی تصاویر و مثال‌ها نیز از همان فصل اقتباس شده‌اند).نکته : خوندن این مقاله به افرادی که مباحث ابتدایی برنامه نویسی شی گرا را نمیدونند توصیه نمیشود. خوندن این مقاله نیازمند دانستن مباحث و مفاهیم ابتدایی شی گرا هست.در این مقاله خواهید خواند:نحوه ایجاد اشیاء سرویستزریق و اعتبارسنجی وابستگی‌ها و مقادیر پیکربندیارتقاء آرگومان‌های اختیاری constructor به آرگومان‌های اجباریآشکار کردن وابستگی‌های پنهانطراحی سرویس‌ها به‌گونه‌ای که immutable باشند1. دو نوع آبجکت در معماری نرم‌افزاردر معماری object-oriented معمولاً با دو نوع آبجکت مواجه هستیم:نوع اول: Service object که عملیاتی را انجام می‌دهد یا بخشی از داده را بازمی‌گرداند.نوع دوم: آبجکت‌هایی که داده‌ها را نگهداری می‌کنند و به صورت اختیاری رفتارهایی برای بازیابی یا تغییر آن داده‌ها ارائه می‌دهند.سرویس‌ها قابلیت این را دارند که یک بار ساخته شوند و برای همیشه قابل استفاده باشند. هر بار استفاده از آنها هیچ تغییری در وضعیت داخلی سرویس ایجاد نمی‌کند.آبجکت نوع دوم می‌تواند پیچیده‌تر از نوع اول باشد، زیرا این آبجکت‌ها توسط سرویس‌ها به کار گرفته می‌شوند تا کار خود را تکمیل کنند. پس از انجام عملیات، به صورت اختیاری تغییری در آبجکت اعمال می‌شود یا نمی‌شود و سپس به سرویس بعدی ارسال می‌شود، مانند شکل زیر:این UML به وضوح نشان می‌دهد که چگونه سرویس‌ها سرویس‌های دیگر را فراخوانی می‌کنند. آبجکت‌ها به عنوان آرگومان متدها داده می‌شوند یا به عنوان داده بازگشتی برمی‌گردانند.نکته مهم: ممکن است یک آبجکت در داخل هر سرویسی که آن را دریافت می‌کند تغییر یابد یا داده‌ای از آن استخراج شود.نام‌گذاری سرویس‌ها معمولاً به کاری که انجام می‌دهند اشاره دارد، مانند: Controller، FileManager، Router، Calculator. بنابراین در نام‌گذاری سرویس‌ها دقت ویژه‌ای داشته باشید.2. تزریق وابستگی‌ها و تنظیمات سرویس از طریق Constructorمعمولاً هر سرویس برای انجام وظیفه خود به سرویس‌های دیگر و یا تنظیمات خاصی نیاز دارد. این وابستگی‌ها باید توسط پارامترهای constructor تزریق و مقداردهی شوند.دلیل این کار آن است که سرویس پس از ساخته شدن آماده انجام کار باشد و نیازی به هیچ تنظیمات اضافی نداشته باشد. برای مثال به سرویس FileLogger نگاه کنید:interface Logger {
    public function log(string $message): void;
}

interface Formatter {
    public function format(string $message): string;
}

final class FileLogger implements Logger {
    private Formatter $formatter;
    private string $logFilePath;

    public function __construct(Formatter $formatter, string $logFilePath) {
        $this-&gt;formatter = $formatter;
        $this-&gt;logFilePath = $logFilePath;
    }

    public function log(string $message): void {
        $formattedMessage = $this-&gt;formatter-&gt;format($message);
        file_put_contents($this-&gt;logFilePath, $formattedMessage, FILE_APPEND);
    }
}

$logger = new FileLogger(new SomeFormatter(), &#039;/path/to/log.txt&#039;);
$logger-&gt;log(&#039;A message&#039;);Formatter وابستگی FileLogger است که از طریق constructor تزریق شده و LogFilePath تنظیمات این سرویس محسوب می‌شود.نکته: این مقادیر پیکربندی ممکن است به طور سراسری در اپلیکیشن شما وجود داشته باشند، مثلاً در نوعی Setting Object یا مدل‌های ساختار داده بزرگ.توجه کنید که کل Configuration Object را تزریق نکنید و فقط همان تنظیمات مورد نیاز سرویس را تزریق کنید.نگهداری مقادیر پیکربندی مرتبطیک سرویس نباید کل یک آبجکت Configuration را دریافت کند. با این حال، بعضی از مقادیر پیکربندی وجود دارند که همیشه به طور همزمان استفاده می‌شوند. تزریق جداگانه آنها انسجام طبیعی آنها را مختل می‌کند. برای رفع این مشکل می‌توان یک آبجکت پیکربندی اختصاصی تعریف، مقداردهی و تزریق کرد، برای مثال: final class MySQLTableGateway
 {
    public function __construct(
        string host,
        int port,
        string username,
        string password,
        string database,
        string table
    ) {
    //...
    }
 }در اینجا مقادیر پیکربندی اتصال به MySQL به صورت تکی داده شده که انسجام کاهش می‌یابد. final class MySQLTableGateway
 {
    public function __construct(
        ConnectionConfiguration connectionConfiguration,
        string table
    ) {
    // ...
    }
 }در اینجا مقادیر مرتبط با هم در یک آبجکت اختصاصی تعریف شده‌اند (به جز Table چون نام جدول بخشی از اطلاعات مورد نیاز برای برقراری ارتباط با پایگاه داده نیست).3. هر چیزی که نیاز دارید را تزریق کنید، نه از آنجایی که می‌گیریدهر فریم‌ورک یا کتابخانه‌ای که استفاده می‌کنید، به اندازه کافی بزرگ یا پیچیده باشد، نوع خاصی از آبجکت را ارائه می‌دهد که هر سرویس و مقادیر پیکربندی که ممکن است بخواهید استفاده کنید را در داخل خود نگهداری می‌کند.نام‌های رایج برای چنین آبجکتی شامل: Service Locator، Manager، Registry، Container می‌باشد.Service Locator چیست؟یک Service Locator خود یک سرویس است که وظیفه بازیابی سرویس‌های دیگر را بر عهده دارد. مثال ساده‌ای از Service Locator:final class ServiceLocator {
    private array $services;

    public function __construct() {
        $this-&gt;services = [
            &#039;logger&#039; =&gt; new FileLogger(/* ... */)
        ];
    }

    public function get(string $identifier): object {
        if (!isset($this-&gt;services[$identifier])) {
            throw new LogicException(&#039;Unknown service: &#039; . $identifier);
        }

        return $this-&gt;services[$identifier];
    }
}
در اینجا متد get() کلید آن سرویسی که مدنظر است را می‌گیرد و اگر موجود نباشد Exception پرتاب می‌کند.نکته: Service Locator مثل نقشه‌ای است که می‌توانید سرویس‌های مدنظرتان را با آن دریافت کنید. کلید معمولاً نام یا رابط کلاسی است که می‌خواهید دریافت کنید. معمولاً در Service Locatorهای واقعی (نه مثال) تمام مسئولیت نمونه‌سازی و مقداردهی پارامترهای constructor یک سرویس را بر عهده می‌گیرند.حال ممکن است وسوسه شوید این Service Locator را تزریق کنید، اما این کار باعث می‌شود کد شما از آن فراخوانی بگیرد. به فرض مثال، HomePageController شما نیاز دارد بداند چطور سرویس مورد نظر خود را صدا بزند و همچنین دسترسی به تعداد زیادی آبجکت و چیزهای نامربوط از طریق Service Locator پیدا می‌کند.برای جلوگیری از تمامی این مشکلات می‌توانیم از قاعده زیر پیروی کنیم:هر زمان که سرویسی برای انجام کارش به سرویس دیگری نیاز داشت، باید به طور صریح و واضح آن را به عنوان وابستگی اعلام کند و از طریق پارامترهای constructor آن را دریافت کند، مانند مثال زیر که یک Controller است:final class HomepageController
{
    private EntityManager $entityManager;
    private ResponseFactory $responseFactory;
    private TemplateRenderer $templateRenderer;

    public function __construct(
        EntityManager $entityManager,
        ResponseFactory $responseFactory,
        TemplateRenderer $templateRenderer
    ) {
        $this-&gt;entityManager = $entityManager;
        $this-&gt;responseFactory = $responseFactory;
        $this-&gt;templateRenderer = $templateRenderer;
    }

    public function execute(Request $request): Response
    {
        $user = $this-&gt;entityManager
            -&gt;getRepository(User::class)
            -&gt;getById($request-&gt;get(&#039;userId&#039;));

        return $this-&gt;responseFactory
            -&gt;create()
            -&gt;withContent(
                $this-&gt;templateRenderer-&gt;render(
                    &#039;homepage.html.twig&#039;,
                    [
                        &#039;user&#039; =&gt; $user
                    ]
                ),
                &#039;text/html&#039;
            );
    }
}Service Locator در اینجا نیاز اصلی ما نیست و نیازهای اصلی را به صورت واضح بیان می‌کنیم.در نسخه اولیه، اگر می‌خواستیم Service Locator را دریافت کنیم، Controller به نظر می‌رسید یک وابستگی دارد. حال در این نسخه، وقتی Service Locator را حذف کردیم، مشخص شد سه وابستگی داریم.حال که در مورد وابستگی واقعی و صادقانه صحبت می‌شود، می‌توانیم و باید یک تکرار دیگر انجام دهیم. ما به EntityManager برای این نیاز داریم که فقط User Repository را دریافت کنیم. پس به جای آن، خود Repository User را دریافت می‌کنیم:final class HomepageController
{
    private UserRepository $userRepository;
    // ...

    public function __construct(
        UserRepository $userRepository
        // ...
    ) {
        $this-&gt;userRepository = $userRepository;
        // ...
    }

    public function execute(Request $request): Response
    {
        $user = $this-&gt;userRepository
            -&gt;getById($request-&gt;get(&#039;userId&#039;));
        // ...
    }
}
در اینجا وابستگی Controller کاملاً واضح و صریح است.4. تمام آرگومان‌های Constructor باید الزامی باشندگاهی اوقات ممکن است فکر کنید وابستگی اختیاری است و آبجکت بدون آن وابستگی نیز می‌تواند به خوبی کار کند. ممکن است یک وابستگی را دغدغه ثانویه در حال حاضر در نظر بگیرید و آن را در پارامترهای constructor اختیاری قرار دهید. اما این کار باعث می‌شود کد ما به طور غیرضروری پیچیده شود در داخل کلاس. هر زمان بخواهید با آن وابستگی کار کنید، باید بررسی کنید آن مقداردهی شده یا خیر. اگر بررسی نکنید، یک خطای Fatal دریافت می‌کنید، مثال:final class BankStatementImporter
{
    private ?Logger $logger;

    public function __construct(?Logger $logger = null)
    {
        $this-&gt;logger = $logger;
    }

    public function import(string $bankStatementFilePath): void
    {
        // Import the bank statement file

        // Every now and then log some information for debugging...
        if ($this-&gt;logger instanceof Logger) {
            $this-&gt;logger-&gt;log(&#039;A message&#039;);
        }
    }
}

$importer = new BankStatementImporter();در اینجا، زمان ساخت BankStatementImporter نیازی به ارسال آرگومان اجباری نیست، اما در داخل کد پیچیدگی اضافه می‌شود.برای رفع این موضوع باید تمام آرگومان‌های constructor را اجباری تعریف کنیم. این موضوع برای تنظیمات (config) نیز صدق می‌کند.مثلاً نباید آرگومان constructor برای تنظیمات را مقدار پیش‌فرض قرار دهید یا بدتر از این، مقدار پیش‌فرض را در داخل کد پیاده‌سازی کنید، مانند مثال زیر:final class FileLogger implements Logger
{
    private ?string $logFilePath;

    public function __construct(?string $logFilePath = null)
    {
        $this-&gt;logFilePath = $logFilePath;
    }

    public function log(string $message): void
    {
        // ...

        file_put_contents(
            $this-&gt;logFilePath !== null ? $this-&gt;logFilePath : &#039;/tmp/app.log&#039;,
            $formattedMessage,
            FILE_APPEND
        );
    }
}در این کد، کلاس FileLogger فردی که از آن استفاده می‌کند اگر بخواهد بداند مسیر پیش‌فرض چیست، باید به داخل کد نگاهی بیندازد. همچنین مسیر پیش‌فرض اکنون جزو پیاده‌سازی کد در نظر گرفته می‌شود و به راحتی می‌تواند تغییر کند بدون اینکه فردی که از آن کلاس استفاده می‌کند متوجه شود. پس برای رفع این مشکل، تمام آرگومان‌های constructor را اجباری کنید تا فرد خودش انتخاب کند. در آن موقع با یک نگاه به آرگومان‌های constructor می‌فهمد چه تنظیمات و وابستگی‌هایی در خود استفاده کرده است.5. فقط برای تزریق از Constructor استفاده کنیدیک ترفند که می‌توان برای وابستگی‌های اختیاری گفت، تزریق وابستگی‌ها توسط Setterها است که اجازه می‌دهد پس از ساخته شدن کلاس، در صورت تمایل، وابستگی را تزریق کنید، مانند کد زیر:final class BankStatementImporter
{
    private ?Logger $logger = null;

    public function __construct()
    {
    }

    public function setLogger(Logger $logger): void
    {
        $this-&gt;logger = $logger;
    }

    // ...
}

$importer = new BankStatementImporter();
$importer-&gt;setLogger($logger);این علاوه بر اینکه مثل موضوع قبلی کد را پیچیده می‌کند، دو قاعده دیگر که در ادامه مقاله به آنها می‌پردازیم را نیز نقض می‌کند:آبجکتی در حالت ناقص نباید بتواند ایجاد شودسرویس‌ها بعد از اینکه ساخته شدند باید غیرقابل تغییر باشندخلاصه اینکه از Setterها برای تزریق استفاده نکنید.6. چیزی به نام وابستگی اختیاری وجود ندارددو بخش قبلی را می‌توان اینطور خلاصه کرد: &quot;شما چیزی به عنوان وابستگی اختیاری ندارید&quot;. شما یا به چیزی وابستگی دارید یا ندارید. حال با این حال، فرض کنید ثبت Log واقعاً یک دغدغه ثانویه است. حال باید چه کار کرد؟در بسیاری از مواقع می‌توانید به استفاده از یک آبجکت جایگزین روی بیاورید که دقیقاً شبیه آبجکت واقعی است اما کاری انجام نمی‌دهد، مانند مثال زیر که NullLogger از رابط Logger استفاده کرده:final class NullLogger implements Logger
{
    public function log(string $message): void
    {
        // Do nothing
    }
}

$importer = new BankStatementImporter(new NullLogger());چنین آبجکت بی‌ضرری معمولاً به عنوان Null Object یا گاهی اوقات Dummy شناخته می‌شود.حال در مورد تنظیمات چطور؟می‌توانید از روش مشابه استفاده کنید. مقادیر تنظیمات هنوز باید اجباری باشند، اما باید راهی برای کاربر آن کلاس فراهم کنید تا مقادیر تنظیمات منطقی مورد نظرش را به دست بیاورد، مانند کد زیر:final class MetadataFactory
{
    public function __construct(Configuration $configuration)
    {
        // ...
    }
}

$metadataFactory = new MetadataFactory(
    Configuration::createDefault()
);به جای اینکه آرگومان سازنده MetaDataFactory را اختیاری کنیم، یک کلاس تنظیمات با مقادیر پیش‌فرض تزریق می‌کنیم.7. تمام وابستگی‌ها را به صورت واضح بیان کنیدشاید فکر کنید تمام وابستگی‌ها و مقادیر تنظیمات را از طریق constructor تزریق کرده‌اید و وابستگی دیگری وجود ندارد. اما هنوز هم ممکن است وابستگی پنهانی وجود داشته باشد. این وابستگی پنهان است و به همین دلیل با یک نگاه سریع به آرگومان‌های constructor نمی‌توان آن را تشخیص داد.1. تبدیل وابستگی‌های استاتیک به آبجکت محوردر اکثر برنامه‌ها، متدهایی داریم که در کل برنامه هستند و در دسترس هستند، مثل: Cache::get(). هر وقت دیدید در سرویس چنین وابستگی استفاده شده، به طوری بازنویسی کنید که از طریق constructor قابل تزریق باشد. این باعث می‌شود تمام وابستگی‌ها سریع و واضح باشند. به کد زیر نگاه کنید:// Before:
final class DashboardController
{
    public function execute(): Response
    {
        $recentPosts = [];

        if (Cache::has(&#039;recent_posts&#039;)) {
            $recentPosts = Cache::get(&#039;recent_posts&#039;);
        }

        // ...
    }
}

// After:
final class DashboardController
{
    private Cache $cache;

    public function __construct(Cache $cache)
    {
        $this-&gt;cache = $cache;
    }

    public function execute(): Response
    {
        $recentPosts = [];

        if ($this-&gt;cache-&gt;has(&#039;recent_posts&#039;)) {
            $recentPosts = $this-&gt;cache-&gt;get(&#039;recent_posts&#039;);
        }

        // ...
    }
}2. تبدیل توابع پیچیده به وابستگی‌های آبجکت محورگاهی اوقات وابستگی پنهان به صورت تابعی وجود دارد که معمولاً این توابع بخشی از آن زبان برنامه‌نویسی که استفاده می‌کنید هستند. پیچیدگی پشت این توابع به طوری است که اگر می‌خواهید خودتان بسازید، شامل سرویس‌های متفاوتی می‌شود و در نهایت برای استفاده‌اش نیز باید تزریق شود. پس این وابستگی پنهان که معمولاً تابع است را باید وابستگی آبجکت محور کرد، مثل مثال زیر که تابع json_encode() است:// Before:
 
final class ResponseFactory
{
    public function createApiResponse(array $data): Response
    {
        return new Response(
            json_encode($data, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT),
            [
                &#039;Content-Type&#039; =&gt; &#039;application/json&#039;
            ]
        );
    }
}

// After:
final class JsonEncoder
{
    /**
     * @throws RuntimeException
     */
    public function encode(array $data): string
    {
        try {
            return json_encode(
                $data,
                JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT
            );
        } catch (RuntimeException $previous) {
            throw new RuntimeException(
                &#039;Failed to encode data: &#039; . var_export($data, true),
                0,
                $previous
            );
        }
    }
}

final class ResponseFactory
{
    private JsonEncoder $jsonEncoder;

    public function __construct(JsonEncoder $jsonEncoder)
    {
        $this-&gt;jsonEncoder = $jsonEncoder;
    }

    public function createApiResponse(array $data): Response
    {
        return new Response(
            $this-&gt;jsonEncoder-&gt;encode($data),
            [
                &#039;Content-Type&#039; =&gt; &#039;application/json&#039;
            ]
        );
    }
}این کار باعث می‌شود کاربر کلاس با یک نگاه به constructor راحت بتواند تصویری از کاری که کلاس انجام می‌دهد را تجسم کند.حال آیا نیازی هست که تمام توابع آبجکت محور شوند؟خیر، نیازی به این کار نیست. در پایین سه معیار می‌آورم که زمانی خواستید این کار را انجام دهید، از خود بپرسید:آیا بعداً می‌خواهید یا ممکن است رفتار ارائه شده توسط این تابع را تغییر دهید یا جایگزین کنید؟آیا رفتار این وابستگی آنقدر پیچیده است که نتوان آن را با چند خط کد ساده پیاده‌سازی کرد؟آیا این تابع با آبجکت سروکار دارد یا نه، فقط مقادیر ساده (رشته و عدد)؟اگر جواب همه اینها بله است، یعنی نیاز به تبدیل به وابستگی آبجکت محور دارید.3. صدا زدن توابع سیستمی را صریح انجام دهیدزیرمجموعه‌ای از توابع و کلاس‌هایی که یک زبان برنامه‌نویسی ارائه می‌دهد، به عنوان وابستگی ضمنی (Implicit Dependency) در نظر گرفته می‌شوند. یعنی با دنیای بیرون اپلیکیشن شما ارتباط دارند، مانند time() و file_get_content().final class MeetupRepository
{
    private Connection connection;

    public function __construct(Connection $connection)
    {
        $this-&gt;connection = $connection;
    }

    public function findUpcomingMeetups(string $area): array
    {
        now = new DateTime();
        return $this-&gt;findMeetupsScheduledAfter(now, $area);
    }

    public function findMeetupsScheduledAfter(
        DateTime $time,
        string $area
    ): array {
        // ...
    }
}استفاده از زمان در سرویس در کد بالا چیزی نیست که به عنوان وابستگی بتوان آن را از آرگومان‌های constructor تزریق کرد یا توسط آرگومان‌های متد آن را دریافت کرد. پس Time یک وابستگی پنهان است. سرویسی هم در حال حاضر وجود ندارد که بتوان با تزریق آن به آرگومان‌های constructor آن را گرفت. پس سرویس را خودمان می‌سازیم:interface Clock
{
    public function currentTime(): DateTime;
}

final class SystemClock implements Clock
{
    public function currentTime(): DateTime
    {
        return new DateTime();
    }
}


final class MeetupRepository
{
    // ...
    private Clock $clock;

    public function __construct(
        Clock $clock,
        /* ... */
    ) {
        $this-&gt;clock = $clock;
    }

    public function findUpcomingMeetups(string $area): array
    {
        now = $this-&gt;clock-&gt;currentTime();
        // ...
    }
}

$meetupRepository = new MeetupRepository(new SystemClock());
$meetupRepository-&gt;findUpcomingMeetups(&#039;NL&#039;);با انتقال دریافت ساعت سیستم به یک سرویس دیگر، سرویس فعلی ما را نیز تست‌پذیرتر می‌کند. به این دلیل تست‌پذیرتر می‌شود که دیگر نتیجه تستی که می‌خواهیم بنویسیم به زمان اجرای آن تست بستگی ندارد و بعد از تاریخ معینی تست شکست نمی‌خورد. ما می‌توانیم زمان ثابتی را پاس دهیم به کلاس برای تست.8. داده‌های مرتبط با یک Task باید به جای آرگومان‌های Constructor، به صورت آرگومان متد پاس داده شونداگرچه باید یک سرویس تمام وابستگی‌های خود را از طریق آرگومان‌های constructor دریافت کند، بعضی از اطلاعات مربوط به یک وظیفه یا Task خاص (Contextual Information) باید به آرگومان‌های متد پاس داده شوند. برای فهم بیشتر به کلاس پایین نگاه کنید که UserId را از Session که از آرگومان constructor تزریق شده می‌گیرد:final class ContactRepository
{
    private Session session;

    public function __construct(Session session)
    {
        this.session = session;
    }

    public function getAllContacts(): array
    {
        return this.select()
            .where([
                &#039;userId&#039; =&gt; this.session.getUserId(),
                &#039;companyId&#039; =&gt; this.session.get(&#039;companyId&#039;)
            ])
            .getResult();
    }
}در اینجا یک سرویس ContactRepository نمی‌تواند به جز گرفتن مخاطبان جدا از آنچه در Session فعلی وجود دارد، مخاطبان دیگری را بگیرد. این سرویس به عبارت دیگر فقط می‌تواند در یک زمینه (Context) اجرا شود. به این دلیل باید داده مرتبط با یک کار یا وظیفه‌ای به آرگومان متد داده شود تا سرویس بتواند در زمینه‌های مختلف قابل استفاده باشد.نکته: برای اینکه تصمیم بگیرید که باید به آرگومان constructor بدهید یا متد، این سؤال را از خودتان بپرسید:آیا می‌توانم این سرویس را به صورت دسته‌ای (Batch) اجرا کنم بدون اینکه بخواهم آن را از نو بسازم؟در حال حاضر با پرسیدن این سؤال در سرویس فعلی ما، نمی‌توان آن را به صورت دسته‌ای برای دریافت مخاطبان کاربرها و شرکت‌های مختلف استفاده کرد. پس باید به آرگومان‌های متد پاس دهیم، کد زیر را نگاه کنید:public function getAllContacts(string $userId, string $companyId): array
{
    return this.select()
        .where([
            &#039;userId&#039; =&gt; $userId,
            &#039;companyId&#039; =&gt; $companyId
        ])
        .getResult();
}نکته: در واقع واژه &quot;Current&quot; (فعلی یا جاری) سیگنال مفیدی است که نشان می‌دهد این اطلاعات زمینه‌ای (Contextual) هستند که باید به آرگومان‌های متد پاس داده شوند.9. نباید رفتار سرویس پس از ایجاد تغییر کندهمانطور که بالاتر گفتیم، با تزریق وابستگی اختیاری پس از ایجاد سرویس، رفتار آن سرویس را تغییر می‌دهیم و این باعث غیرقابل پیش‌بینی بودن سرویس می‌شود. این موضوع درباره متدهایی که چیزی تزریق نمی‌کنند اما از خارج رفتار سرویس را تغییر می‌دهند نیز صدق می‌کند، مثل مثال زیر:final class Importer
{
    private bool $ignoreErrors = true;

    public function ignoreErrors(bool $ignoreErrors): void
    {
        $this-&gt;ignoreErrors = $ignoreErrors;
    }

    // ...
}

$importer = new Importer();
// ...
$importer-&gt;ignoreErrors(false);
// ...در اینجا ما هر وقت بخواهیم می‌توانیم رفتار سرویس را تغییر دهیم. باید مطمئن شد چنین اتفاقی نمی‌افتد. همیشه باید زمان ایجاد، تمامی وابستگی‌ها و مقادیر تنظیمات وجود داشته باشند و پس از ساخت سرویس، دوباره قابل تغییر تنظیمات نباشند.اجازه دادن تغییر یافتن پس از ایجاد به سرویسی باعث می‌شود برای کسانی که از آن سرویس استفاده می‌کنند غیرقابل پیش‌بینی شود. هر بار مسیر اجرایی آن سرویس تغییر کند، مانند عکس زیر:اگر شما جوری سرویسی را درست کنید که تغییر نکند و وابستگی اختیاری نداشته باشد، باعث می‌شود نتیجه آن سرویس در طول زمان قابل پیش‌بینی باشد و بر اساس اینکه چگونه یک شخص آن را فراخوانی می‌کند، رفتارش تغییر نکند.موضوع مهم: &quot;شاید در اپلیکیشنی که من می‌سازم واقعاً به سرویس‌های قابل تغییر نیاز دارم&quot;در اپلیکیشن‌های سمت وب عمدتاً اصلاً نیازی نیست سرویس قابل تغییر داشته باشید و در زمان ساخت سرویس به راحتی می‌توان رفتار آن سرویس را تعیین کرد.اما ممکن است در اپلیکیشن‌های دیگر مثل بازی‌سازی یا موارد دیگر، زمانی که کاربر یک کار را انجام داد، با استفاده از متدهای سرویس‌تان بخواهید چیزی از آن حذف یا اضافه کنید. در این مورد توصیه می‌شود که درباره روش‌هایی فکر کنید که اجازه ندهید آبجکت‌ها از طریق متدهای عمومی، رفتار سایر آبجکت‌ها را دوباره تنظیم کنند.10. در Constructor جز مقداردهی Properties هیچ کاری انجام ندهیدساختن یک سرویس به معنای مقداردهی ورودی‌های constructor است تا سرویس آماده شود. کار واقعی که سرویس انجام می‌دهد در متدهای آن انجام می‌شود. اما شاید وسوسه شوید که کاری را در constructor آن سرویس انجام دهید، مانند ساخت یک دایرکتوری اگر وجود نداشت:final class FileLogger implements Logger
{
    private string $logFilePath;

    public function __construct(string $logFilePath)
    {
        $logFileDirectory = dirname($logFilePath);
        if (!is_dir($logFileDirectory)) {
            mkdir($logFileDirectory, 0777, true);
        }

        touch($logFilePath);
        $this-&gt;logFilePath = $logFilePath;
    }

    // ...
}در اینجا یک آبجکت بر روی فایل‌های سیستم تأثیر می‌گذارد حتی اگر هیچ وقت متدی از آن فراخوانی نشود. رفتار خوب در طراحی آبجکت این است که جز اعتبارسنجی آرگومان‌های constructor و سپس مقداردهی Properties، کاری انجام نشود. حتی ممکن است فکر کنید با انتقال ساخت دایرکتوری به یکی از متدهای Private یا داخلی سرویس، کاملاً مشکل حل می‌شود. تا حدودی از حالت اول بهتر است، اما بهترین کار این نیست. بهترین کار این است که قبل از ساخت FileLogger این دایرکتوری را داشته باشیم. پس آن را کلاً به بیرون constructor و سرویس می‌آوریم:final class FileLogger implements Logger
{
    private string $logFilePath;

    public function __construct(string $logFilePath)
    {
        if (!is_writable(logFilePath)) {
            throw new InvalidArgumentException(
                &quot;Log file path \&quot;$logFilePath\&quot; should be writable&quot;
            );
        }

        $this-&gt;logFilePath = $logFilePath;
    }

    public function log(string $message): void
    {
        // ...
    }
}در اینجا برای ایجاد FileLogger انتظار داریم که از قبل فایل ما ایجاد شده باشد و قابل نوشتن باشد. باید به این توجه داشته باشید که انتقال تنظیمات FileLogger به بیرون constructor آن، قرارداد را عوض می‌کند.در حالت قبلی ما مسیر فایل Log را می‌دادیم و انتظار می‌رفت که تمام کارها را خود FileLogger انجام دهد، مانند ساخت دایرکتوری. اما در حالت فعلی، FileLogger انتظار دارد مسیر فایلی که می‌دهید از قبل وجود داشته باشد و قابل نوشتن باشد.بیایید نگاهی بیندازیم به مثالی که ظریف‌تر از آبجکتی است که داخل constructor خودش کاری انجام می‌دهد:final class Mailer
{
    private Translator $translator;
    private string $defaultSubject;

    public function __construct(Translator $translator)
    {
        $this-&gt;translator = $translator;

        // ...

        $this-&gt;defaultSubject = $this-&gt;translator
            -&gt;translate(&#039;default_subject&#039;);
    }

    // ...
}اگر ترتیب تخصیص‌ها را تغییر دهید، چه اتفاقی می‌افتد؟حال translate() بر روی null صدا زده می‌شود و یک خطای Fatal دریافت می‌کنید. به همین دلیل قاعده &quot;فقط در constructor ویژگی‌ها (Properties) را مقداردهی کن&quot; وجود دارد، چون ممکن است این مقداردهی ترتیبی باشد و این یعنی شما در constructor‌تان کار (منطق یا عملیات) انجام داده‌اید که نباید انجام می‌دادید.11. یک Exception پرتاب کنید زمانی که یک آرگومان نامعتبر استزمانی که کلاینت (کدی که از کلاس استفاده می‌کند) آرگومانی نامعتبر بدهد، معمولاً Type Checkerها آن را بررسی می‌کنند و به شما اخطار می‌دهند، مثلاً اگر انتظار String باشد اما Boolean پاس داده شود.اما تکیه بر Type Systemها کافی نیست. ممکن است داده‌ای که پاس می‌دهد درست باشد اما واقع‌گرایانه نباشد که بشود از آن استفاده کرد. تصمیم برای پرتاب نکردن Exception نیز می‌تواند گزینه‌ای باشد، به شرط اینکه باعث خراب شدن کار آبجکت در مراحل بعدی نشود. برای مثال Router زیر را ببینید:final class Router
{
    private array $controllers;
    private string $notFoundController;

    public function __construct(array $controllers, string $notFoundController)
    {
        // Should we check if the controllers array is empty?
        $this-&gt;controllers = $controllers;
        $this-&gt;notFoundController = $notFoundController;
    }

    public function match(string $uri): string
    {
        foreach ($this-&gt;controllers as $pattern =&gt; $controller) {
            if ($this-&gt;matches($uri, $pattern)) {
                return $controller;
            }
        }

        return $this-&gt;notFoundController;
    }

    private function matches(string $uri, string $pattern): bool
    {
        // ...
    }
}

$router = new Router(
    [
        &#039;/&#039; =&gt; &#039;homepage_controller&#039;
    ],
    &#039;not-found&#039;
);

$router-&gt;match(&#039;/&#039;);در اینجا آیا نیاز داریم آرگومان Controllers را واقعاً Validate (اعتبارسنجی) کنیم که شامل یک جفت URL و Controller باشد؟قاعدتاً نه، چون خالی بودن آرایه Controllers باعث خراب شدن کار Router نمی‌شود. اگر خالی باشد، تابع match() کنترلر Not-Found را برای ما برمی‌گرداند. این رفتاری است که از Router انتظار می‌رود، بنابراین این کار نباید باعث شکست کارهای بعدی Router شود. اما به هر حال باید چک کنید که مقادیر در داخل آرایه همه رشته هستند تا باعث شود اشتباهات برنامه‌نویسی زودتر شناسایی شود:final class Router 
{
    private array $controllers;

    public function __construct(array $controllers)
    {
        foreach (array_keys($controllers) as $pattern) {
            if (!is_string($pattern)) {
                throw new InvalidArgumentException(
                    &#039;All URI patterns should be provided as strings&#039;
                );
            }
        }

        foreach ($controllers as $controller) {
            if (!is_string($controller)) {
                throw new InvalidArgumentException(
                    &#039;All controllers should be provided as strings&#039;
                );
            }
        }

        $this-&gt;controllers = $controllers;
    }

    // ...
}حتی می‌توانید این را طوری بنویسید که از Type Systemها استفاده شود با اضافه کردن تابع addController() خصوصی (Private) به کلاس. پارامترهای رشته‌ای نظیر Pattern و Controller آنها را جفت کنید:final class Router
{
    private array $controllers = [];

    public function __construct(array $controllers)
    {
        foreach ($controllers as $pattern =&gt; $controller) {
            $this-&gt;addController($pattern, $controller);
        }
    }

    private function addController(string $pattern, string $controller): void
    {
        $this-&gt;controllers[$pattern] = $controller;
    }

    // ...
}12. سرویس‌ها را تغییرناپذیر و به شکل گراف و فقط با چند نقطه ورودی تعریف کنیدزمانی که فریم‌ورک اپلیکیشن، Controller شما را صدا می‌زند، می‌توانید فرض کنید تمام وابستگی‌ها مشخص هستند. برای مثال، یک Controller به Repository‌هایی نیاز دارد که داده مورد نظرش را دریافت کند یا نیاز به Template Engine دارد که قالب خود را رندر کند و غیره. تمام این وابستگی‌ها وقتی با دقت در پارامترهای constructor تعریف شوند، می‌توانند یکباره ایجاد شوند که معمولاً باعث ایجاد یک گراف بزرگ از آبجکت‌ها می‌شود. اگر فریم‌ورک یک Controller دیگری را فراخوانی کند، از گراف متفاوتی از آبجکت‌ها برای انجام وظیفه خودش استفاده می‌کند. پس می‌توان Controller‌ها را به عنوان نقطه ورودی گراف آبجکت برنامه در نظر گرفت:این گراف شامل تمامی سرویس‌های یک برنامه است و سرویس Controller نقطه ورود این گراف است. Controller تنها سرویسی است که به صورت مستقیم می‌شود آن را بازیابی کرد. تمامی سرویس‌ها جز Controller باید فقط برای این در دسترس باشند که به عنوان وابستگی تزریق شوند.بیشتر برنامه‌ها چیزی شبیه Service Container دارند که توضیح می‌دهد چگونه تمام سرویس‌های برنامه ساخته شوند، وابستگی آنها چه چیزهایی هستند و چگونه می‌توان آنها را ساخت. این Service Container همچنین می‌تواند به عنوان Service Locator نیز عمل کند. می‌توانید از آن بخواهید که یکی از سرویس‌هایش را برگرداند تا بتوانید از آن استفاده کنید. در بالاتر خواندیم که فقط باید وابستگی‌های واقعی را تزریق کنید با فرض موارد زیر:همه سرویس‌های اپلیکیشن، یک گراف بزرگ از آبجکت را تشکیل می‌دهندنقطه ورودی برنامه و گراف ما، Controller‌ها هستندهیچ سرویسی نباید برای گرفتن سرویس‌های دیگر به سراغ Service Locator برودطبق این سه مورد نتیجه می‌گیریم که Service Container متدهای عمومی‌اش فقط برای گرفتن Controller‌ها باشد و بقیه متدهای آن که سرویس‌های دیگر جز Controller را درست می‌کنند باید خصوصی (Private) باشند، چون فقط به عنوان وابستگی به Controller‌ها داده می‌شوند. برای ملموس‌تر شدن به کد زیر توجه کنید:final class ServiceContainer
{
    public function homepageController(): HomepageController
    {
        return new HomepageController(
            $this-&gt;userRepository(),
            $this-&gt;responseFactory(),
            $this-&gt;templateRenderer()
        );
    }

    private function userRepository(): UserRepository
    {
        // ...
    }

    private function responseFactory(): ResponseFactory
    {
        // ...
    }

    private function templateRenderer(): TemplateRenderer
    {
        // ...
    }

    // ...
}

if ($uri == &#039;/&#039;) {
    $controller = $serviceContainer-&gt;homepageController();
    $response = $controller-&gt;execute($request);
    // ...
} elseif (/* ... */) {
    // ...
}یک Service Container امکان این را نیز می‌دهد که از سرویس‌ها استفاده مجدد داشته باشیم (یعنی یک بار ساخته شود و سپس تا هر چند استفاده شود). به همین دلیل هر کدام از Controller‌ها به عنوان نقطه ورودی، شاخه گراف آبجکت مستقلی ندارند. برای مثال، یک Controller دیگری نیز جز HomePageController می‌تواند از TemplateRenderer استفاده کند. به همین دلیل باید سرویس‌ها را به گونه‌ای طراحی کنیم که پیش‌بینی‌پذیر باشند. اگر موضوعاتی که در این مقاله گفته شد را رعایت کنید، به یک گراف آبجکت خواهید رسید که می‌توانیم آن را یک بار ایجاد و سپس بارها استفاده کنیم.خلاصه مطلبسرویس‌ها باید در یک مرحله ساخته شوند، به طوری که تمام وابستگی‌ها و مقادیر پیکربندی (Config) آنها به صورت آرگومان‌های constructor فراهم شود. تمام وابستگی‌های سرویس باید شفاف و مشخص باشند و باید به صورت شی (Object) تزریق شوند. تمام مقادیر پیکربندی (Config) باید اعتبارسنجی شوند. وقتی constructor آرگومانی دریافت کند که به هر نحوی نامعتبر است، باید یک Exception پرتاب کند.پس از ساخته شدن، سرویس باید تغییرناپذیر باشد، به طوری که رفتار آن با فراخوانی هیچ متدی نباید تغییر کند.تمام سرویس‌های اپلیکیشن در کنار هم یک گراف بزرگ و تغییرناپذیر آبجکت می‌سازند که معمولاً توسط یک Service Container مدیریت می‌شود. Controller‌ها نقاط ورود این گراف هستند. سرویس‌ها می‌توانند فقط یک بار ساخته شوند و چندین بار مورد استفاده قرار گیرند.نتیجه‌گیری و پیشنهاداتاگر تا اینجای مقاله را خوانده و دنبال کرده‌اید، یعنی به مطالب مطرح شده علاقه دارید. در این مقاله سعی شد موضوعات به صورت خلاصه بیان شوند، طوری که نکات مهم آن از قلم نیفتد. اما بسنده کردن به اطلاعاتی که اینجا در مورد بخش کوچکی از دنیای جذاب OOP گفته شد، کار اشتباهی است.از طرف من به شما پیشنهاد می‌شود که به دانش بیشتر در مورد OOP روی بیاورید و یک چیز سطحی که فقط یک کلاس و متد باشد را OOP در نظر نگیرید. موضوعاتی که اینجا گفته شد نیاز به تمرین در پروژه‌های واقعی دارد. اگر هم نمی‌توانید و دسترسی به پروژه واقعی ندارید، یک پروژه تمرینی نزدیک به پروژه واقعی برای خود تعریف کنید که در آنجا موضوعاتی که گفته شد را به کار بگیرید.در نهایت، آخرین سخن من به شما: با یادگیری عمیق‌تر این مباحث یا هر مبحث دیگری، شما را از انبوه برنامه‌نویسان جدا می‌کند و تمایزی در خود نسبت به بقیه ایجاد می‌کنید. این تخصص عمیق همان چیزی است که شما را از یک برنامه‌نویس معمولی به یک توسعه‌دهنده حرفه‌ای تبدیل می‌کند.</description>
                <category>MohamadReza Salehi</category>
                <author>MohamadReza Salehi</author>
                <pubDate>Sun, 03 Aug 2025 22:09:16 +0330</pubDate>
            </item>
            </channel>
</rss>