سرفصل‌های آموزشی
آموزش الگوهای طراحی (Design Pattern)
الگوی طراحی Iterator

الگوی طراحی Iterator

درباره ی الگو های طراحی 

در مهندسی نرم افزار، برنامه نویسان گاهی با مشکلات مشترکی دست و پنجه نرم می کنند. با فراگیرتر شدن این مشکلات و بحث و تبادل راه حل های برنامه نویسان با هم، کم کم راه حلی عمومی برای این مشکلات ارائه می شود. در صورتی که این راه حل ها به مقبولیت بالایی دست یابند در آخر آن ها را نام گذاری می کنند و به عنوان یک الگوی طراحی (Design Pattern) شناخته می شوند. پس الگوهای طراحی درواقع پاسخی عمومی و تکرارپذیر به سوالاتی است که بین برنامه نویسان مشترک است. الگوهای طراحی، طراحی آماده ای نیست که شما به صورت آماده به پروژه ی خود اضافه کنید و بگویید اکنون پروژه از آن الگوی طراحی پیروی می کند. بلکه همچون شابلونی است که معیارهای مختلفی که باید رعایت شود تا فرایند توسعه راحت تر شود را مشخص می کند.

یکی از دسته بندی هایی که برای الگوهای طراحی استفاده می شود، الگو های طراحی را به سه دسته ی creational، structural و behavioral تقسیم می کند. دسته ی اول یعنی creational به سازوکار های ایجاد اشیا (objects) می پردازد تا انعطاف و استفاده پذیری مجدد کد موجود را افزایش دهد. الگوهای طراحی structural روش هایی را نشان می دهند تا اشیا و کلاس ها (classes) را با هم ترکیب کنیم و به ساختار هایی بزرگ تر و پیچیده تر دست یابیم درحالی که این ساختارها همچنان کارآمد و منعطف باقی بمانند. دسته ی آخر یعنی الگوهای طراحی behavioral به تعامل بین اشیا و توزیع مسئولیت ها بین آن ها می پردازد و سعی بر بهینه کردن این تعاملات دارد.

در این مقاله یکی از الگوهای طراحی behavioral، یعنی الگوی طراحی Iterator را بیشتر توضیح می دهیم و شیوه ی استفاده از آن را با یک مثال نشان می دهیم.

الگوی طراحی Iterator

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

طرح مسئله

در تمام مدت کد زدن، شما به مجموعه داده ها (Collections) برمی خورید و از آن ها استفاده می کنید. در واقع این مجموعه داده ها فقط ظرفی برای نگهداری اشیا (Objects) هستند. خیلی از مجموعه داده ها، عناصر خود را در یک لیست ساده نگهداری می کنند؛ اما ممکن است از ساختارهای دیگری همچون ساختار درختی، گراف یا ساختارهای پیچیده تری استفاده کنند. به هر حال شما نیاز دارید تا بتوانید، از طریقی به اعضای این مجموعه دسترسی داشته باشید، بدون اینکه یک عضو را بارها ببینید. اگر با مجموعه داده هایی کار کنید که عناصر خود را در یک لیست نگهداری می کنند. این کار بسیار ساده است و شما به طور صرف به ترتیب عنصر ها را برمی دارید. اما اگر مجموعه داده ی ما پیچیده تر بود چه؟ در آن صورت چگونه می خواهید تا به ترتیب به عناصر آن دسترسی داشته باشید؟ یک ساختار درختی را در نظر بگیرید. ممکن است شما در اول کار با دیدگاه مبتنی بر عمق، عناصر را در دسترس قرار دهید. پس از مدتی نیاز دارید تا با دیدگاه مبتنی بر عرض به داده ها دسترسی داشته باشید و شاید در آینده به دسترسی تصادفی هم نیاز پیدا کنید.

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

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

در راستای حل این مشکلات (یعنی جدا سازی منطق پیمایش) الگوی طراحی Iterator، راه حل را جلوی پای ما می گذارد.

الگوی طراحی Iterator ، راه حل مشکل

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

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

از آنجایی که مجموعه داده ها (collections) از گروهی از اعضا به وجود می آیند، به آن ها aggregate (تجمیع شده) هم می گویند. رابط Iterator اجبار می کند که هر کلاس Iterator تعدادی متد مشخص را پیاده سازی کند. برای مثال در دیاگرام بالا متد next برای خروجی دادن عضو بعدی و متد hasNext برای چک کردن این که آیا عضوی باقی مانده است یا خیر استفاده می شوند. کلاس هایی که رابط Iterator را دنبال می کنند باید این متدها را تعریف کنند. مجموعه داده نیز باید بتواند یک Iterator ارائه دهد. در مثال آخر مقاله به خوبی این موارد نشان داده شده اند. پس نگران نباشید.

نکات مثبت و منفی

با استفاده از این الگوی طراحی در واقع از دو قانون از قواعد SOLID نیز پیروی کرده ایم. کلاس ها وظایف خاص خود را دارند که یعنی قانون Single Responsibility رعایت شده است. همچنین به راحتی می توان یک Iterator ساخت و درون کد از آن استفاده کرد، بدون اینکه کد با مشکل مواجه شود. این مسئله نیز همان قانون Open/Closed است.

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

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

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

پیاده سازی الگوی طراحی Iterator

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

تمامی Iterator ها باید رابط Iterator را، که درون هسته ی اصلی php می باشد، پیاده سازی کنند. این رابط متدهای زیر را اجباری می کند.

شکل 1 رابط Iterator در php

هنگامی که شما یک حلقه ی foreach را صدا می زنید متدها به ترتیب زیر صدا زده می شوند.

متد rewind یک مرتبه در ابتدای حلقه صدا زده می شود و وظیفه دارد تا نقطه ی آغاز الگوریتم پیمایش را مشخص کند. سپس در هر دور از حلقه، ابتدا متد valid که می گوید آیا چیزی برای خروجی وجود دارد یا نه، سپس current که عنصری را به ما می دهد که نوبتش است و بعد از آن متد key که کلید این عنصر را می دهد. در آخر هر حلقه نیز متد next صدا زده می شود، که موقعیت عنصر بعدی را پیدا می کند.

این رابط برای وقتی بود که شما بخواهید به اصطلاح، به صورت درونی شیء را پیمایش کنید. برای حالت خارجی می بایست رابط IteratorAggregate را پیاده سازی کنید. این رابط به صورت زیر است:

شکل 2 رابط IteratorAggregate در php

خروجی تابع getIterator می بایست یک شیء قابل پیمایش باشد.

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

ابتدا یک کلاس با نام Movie می سازیم. این کلاس همان کلاسی است که فیلم های ما، یک نمونه از آن هستند.

class Movie
{
    /**
     * @var string
     */
    private $title;

    /**
     * @var float
     */
    private $rating;

    /**
     * Movie constructor.
     * @param string $title
     * @param float $rating
     */
    public function __construct(string $title, float $rating)
    {
        $this->title = $title;
        $this->rating = $rating;
    }

    /**
     * @return string
     */
    public function title()
    {
        return $this->title;
    }

    /**
     * @return float
     */
    public function rating()
    {
        return $this->rating;
    }
}

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

class Movies implements \IteratorAggregate
{
    /**
     * @var array
     */
    private $list = [];

    /**
     * add a movie to the movies collection
     *
     * @param Movie $movie
     */
    public function add(Movie $movie): void
    {
        $this->list[] = $movie;
    }

    /**
     * Retrieve an external iterator
     * @link https://php.net/manual/en/iteratoraggregate.getiterator.php
     * @return Traversable An instance of an object implementing <b>Iterator</b> or
     * <b>Traversable</b>
     * @since 5.0.0
     */
    public function getIterator(): iterable
    {
        return new MoviesIterator($this->list);
    }

    /**
     * returns an instance of MoviesIterator with reverse order of movies
     * @return MoviesIterator
     */
    public function getReverseIterator(): iterable
    {
        return new MoviesIterator($this->list, true);
    }

    /**
     * returns a traversable collection of movies with specified rating
     * @param float $rating
     * @return MoviesIterator
     */
    public function getRatedIterator(float $rating): iterable
    {
        $filteredMovies = array_filter($this->list, function (Movie $movie) use ($rating) {
            return $movie->rating() === $rating;
        });

        //reindex filteredMovies from zero
        $filteredMovies = array_values($filteredMovies);

        return new MoviesIterator($filteredMovies);
    }
}

هنگامی که حلقه ی foreach روی یک شیء از این کلاس صدا زده شود متد getIterator به صورت خودکار صدا زده می شود. اما خروجی این متد ها یک شیء از جنس MoviesIterator است که در ادامه کد آن را با هم می بینیم. در واقع متدهای این کلاس همه یک شیء قابل پیمایش را باز می گردانند که رابط Iterator را پیاده سازی کرده است. محتویات کلاس MoviesIterator در زیر آمده است.

class MoviesIterator implements \Iterator
{
    /**
     * @var array
     */
    private $movieList;

    /**
     * @var int
     */
    private $position = 0;

    /**
     * MoviesIterator constructor.
     * @param array $list
     * @param bool $reverse
     */
    public function __construct(array $list, bool $reverse = false)
    {
        $this->movieList = $reverse ? array_reverse($list) : $list;
    }

    /**
     * Return the current element
     * @link https://php.net/manual/en/iterator.current.php
     * @return mixed Can return any type.
     * @since 5.0.0
     */
    public function current()
    {
        return $this->movieList[$this->position];
    }

    /**
     * Move forward to next element
     * @link https://php.net/manual/en/iterator.next.php
     * @return void Any returned value is ignored.
     * @since 5.0.0
     */
    public function next(): void
    {
        ++$this->position;
    }

    /**
     * Return the key of the current element
     * @link https://php.net/manual/en/iterator.key.php
     * @return mixed scalar on success, or null on failure.
     * @since 5.0.0
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * Checks if current position is valid
     * @link https://php.net/manual/en/iterator.valid.php
     * @return bool The return value will be casted to boolean and then evaluated.
     * Returns true on success or false on failure.
     * @since 5.0.0
     */
    public function valid(): bool
    {
        return isset($this->movieList[$this->position]);
    }

    /**
     * Rewind the Iterator to the first element
     * @link https://php.net/manual/en/iterator.rewind.php
     * @return void Any returned value is ignored.
     * @since 5.0.0
     */
    public function rewind(): void
    {
        $this->position = 0;
    }
}

همانطور که می بینید متدهای مربوط به رابط Iterator یعنی current, rewind, next و valid پیاده سازی شده اند.

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

// movies
$movie1 = new Movie('Movie1', 2.5);
$movie2 = new Movie('Movie2', 7);
$movie3 = new Movie('Movie3', 8);
$movie4 = new Movie('Movie4', 7);
$movie5 = new Movie('Movie5', 9.5);
$movie6 = new Movie('Movie6', 7);

//movie collection
$movieCollection = new Movies();
//add movies to collection
$movieCollection->add($movie1);
$movieCollection->add($movie2);
$movieCollection->add($movie3);
$movieCollection->add($movie4);
$movieCollection->add($movie5);
$movieCollection->add($movie6);

foreach ($movieCollection as $movie) {
    echo '-' . $movie->title() . '<br/>';
}

//reverse order
echo '<br/>' . 'movies in reverse order' . '<br/>';
foreach ($movieCollection->getReverseIterator() as $movie) {
    echo '-' . $movie->title() . '<br/>';
}

//movies with rate 7 only
echo '<br/>' . 'movies with rating =7 only' . '<br/>';
foreach ($movieCollection->getRatedIterator(7) as $movie) {
    echo '-' . $movie->title() . '<br/>';
}

ما فیلم ها را در سه دسته بندی عادی، برعکس و فیلم های با امتیاز ۷ نشان داده ایم.

اگر دقت کرده باشید در حالت عادی، ما خود مجموعه داده را به حلقه ی foreach دادیم اما این یکی از زیبایی های php است که خود وقتی می بیند مجموعه داده از رابط IteratorAggregate پیروی می کند، متد getIterator را روی آن صدا می زند و حلقه را روی آن اجرا می کند.

خروجی ما به صورت زیر است:

-Movie1 
-Movie2 
-Movie3 
-Movie4 
-Movie5 
-Movie6 

movies in reverse order 
-Movie6 
-Movie5 
-Movie4 
-Movie3 
-Movie2 
-Movie1 

movies with rating =7 only 
-Movie2 
-Movie4 
-Movie6 

جمع بندی

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

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

online-support-icon