method ها توانایی object نیستند!

method ها توانایی object نیستند!

در یکی از پروژه هایی که داشتم نیاز بود تا اطلاعیه هایی رو از طریق ایمیل به تمام کاربر ها ارسال کنم.

کلاس AnnouncementController  من مثل کد زیر بود:

class AnnouncementsController extends Controller
{
    // ...

    public function store()
    {
        $this->validate(request(), [
            'subject' => 'required',
            'message' => 'required',
        ]);

        Announcement::create(request(['subject', 'message']))->broadcast();

        return redirect()->route('admin.announcements.create');
    }
}

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

Announcement::create(request(['subject', 'message']))->broadcast();

و سریعا پیش خودتون فکر کردین:

چی شد؟ یه اطلاعیه نباید توانایی ارسال (broadcast) کردن خودشو داشته باشه.

من هم همین فکر رو می کردم اما طی چند ماه اخیر نظرم عوض شده.

کلاس انجام دهنده ی ترسناک

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

نه، برای ارسال یک اطلاعیه به چیزی نیاز داریم که توانایی ارسال رو داشته باشه. کلاسی مثل Announcer  یا Broadcaster یا حتا AnnouncementBroadcaster :

class AnnouncementBroadcaster
{
    private $queue;

    public function __construct($queue)
    {
        $this->queue = $queue;
    }

    public function broadcastAnnouncement($announcement)
    {
        $this->queue->dispatch(new BroadcastAnnouncement($announcement));
    }
}

این مدل کلاس براتون آشنا هست؟ ممکنه این کلاس رو با اسم AnnouncementService هم بشناسید.

اصلا Method چیه؟

سو تفاهم اصلی اینه که ما فکر می کنیم method ها چیز هایی هستن که یک شی می تونه انجام بده.

اگه بر این باور باشین که method های یک شی توانایی های اون رو نشون میده پس حتما داشتن توانایی ()broadcast برای Announcement احمقانه به نظر می رسه.

اما اگه method چیزی که شی می تونه انجام بده نبود و چیزی بود که می شه با اون شی انجام داد چطور؟

اگه method ها عملکرد هایی بودن که یک شی پتانسیل انجامشون رو ایجاد میکنه پس کاملا با عقل جور در میاد که یک Announcement رو ()broadcast کنیم.

استاندارد دوگانه

تا حالا از متد format کلاس DateTime  استفاده کردین؟

$date = new DateTime('2017-01-23');
$date->format('l F j, Y');
// Monday January 23, 2017

مطمئنم تا حالا به این فکر نکردین که:

یک تاریخ نباید بتونه خودش رو فرمت کنه

با داشتن متد ()format، کلاس DateTime به ما میگه:

من می تونم فرمت بشم!

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

کابوسی به نام DateTimeFormatter 

اگر کلاس AnnouncementBroadcaster که پیش تر بهش اشاره شد رو در نظر بگیرین و سعی کنین همون شیوه تفکر رو در فرمت کردن تاریخ ها به کار بگیرین احتمالا به چیزی شبیه به این می رسین:

class DateTimeFormatter
{
    private $formattingRules;

    public function __construct($formattingRules)
    {
        $this->formattingRules = $formattingRules;
    }

    public function formatDateTime($dateTime, $formatString)
    {
        // ...
    }
}

تصور کنید نیاز باشه تاریخی رو داخل قالب html فرمت کنیم. به عنوان مثال می خواهیم تاریخ انتشار وبلاگ رو نمایش بدیم. از اون جایی که کلاس DateTimeFormatter وابستگی های خودش رو داره باید یک شی از پیش آمده شده رو از کنترلر به فایل View بدیم.

class PostsController
{
    private $dateTimeFormatter;

    public function __construct(DateTimeFormatter $dateTimeFormatter)
    {
        $this->dateTimeFormatter = $dateTimeFormatter;
    }

    // ...
}

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

class PostsController
{
    // ...

    public function show($postId)
    {
        $post = Post::findOrFail($postId);

        return view('posts.show', [
            'post' => $post,
            'dateTimeFormatter' => $dateTimeFormatter,
        ]);
    }
}

در نهایت می تونیم از DateTimeFormatter برای فرمت کردن published_at در قالبمون استفاده کنیم.

<article>
    <h1>{{ $post->title }}</h1>
    <p><small>{{ $dateTimeFormatter->formatDate($post->published_at, 'l F j, Y') }}</small></p>
</article>

همه این ها به خاطر این که تاریخ نباید بتونه خودش رو فرمت کنه. درسته؟

ولی این فرق داره!

شی های DateTime از نوع value object حساب میشن درسته؟

داشتن توانایی انجام ()announcement->broadcast$ بسیار خطرناک تر از date->format('Y-m-d')$ هست. به این دلیل که  broadcast کردن به طور قطع به سرویسی مثل صف وابسته است.

خب باید بگم DateTime  هم وابستگی های عمومی داره!

هر شی DateTime  باید درمورد دیتابیس timezone ها اطلاع داشته باشه تا بدونه کدوم timezone ها در دسترس هستن و چگونه بین اون ها تبدیل انجام بده.

اگه می خواستیم یک شی DateTime از دیتابیس timezone متفاوتی نسبت به شی دیگه استفاده کنه چی؟

در حقیقت نمیشه این کار رو کرد. اما آیا کسی این دغدغه رو داره؟ معلومه که نه!

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

مبارزه با طراحی بد گمان (پارانوئید)

پس چرا ما این حجم از بد گمانی رو نسبت به ()announcement->broadcast$ داریم؟

بله، Announcement یک وابستگی صریح به سرویس صف ای که برای برنامه پیکربندی شده داره.

class Announcement extends Model
{
    protected $guarded = [];

    public function broadcast()
    {
        dispatch(new BroadcastAnnouncement($this));
    }
}

اما چرا به طور پیش فرض فکر می کنیم که این اشتباهه؟

چرا سریع می ریم سراغ AnnouncementBroadcaster حتا اگه این وابستگی واضح هیچ عواقب منفی ای در برنامه ما نداره؟

انعطاف پذیری به اندازه کافی

در این مثال dispatch کننده job از service container لاراول دریافت شده. به همین دلیل استفاده از سرویس های مختلف نسبت به هر محیط بسیار راحت انجام می شه.

$container->bind(Illuminate\Contracts\Bus\Dispatcher::class, function () {
    return new SomeOtherJobDispatcher;
});

این مثال در حال حاضر خیلی بیشتر از دیتابیس timezone ها انعطاف پذیره.

تنها زمانی چالش به وجود میاد که نیاز بشه dispatch کننده job ها رو هنگام اجرای برنامه تنظیم کنیم.

حتا اون موقع هم دریافت dispatch کننده به عنوان پارامتر ورودی از درست کردن AnnouncementBroadcaster راحت تر و منعطف تره.

class Announcement extends Model
{
    protected $guarded = [];

    public function broadcast($dispatcher)
    {
        $dispatcher->dispatch(new BroadcastAnnouncement($this));
    }
}

این کار به شما اجازه میده مدل ذهنی کار هایی که میشه با یک شی انجام داد رو حفظ کنید و همچنان به شما این امکان رو میده که dispatch کننده متفاوتی رو با هر بار صدا زدن متد broadcast انتخاب کنید.

مکان یابی مشترک داده ها و رفتار به صورت پیش فرض

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

AnnouncementBroadcaster رو به عنوان گزینه نهایی در نظر بگیرین نه گزینه پیش فرض. 

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


online-support-icon