خوب از اینجا میریم برای صحبت و بررسی الگوهای طراحی GOF که این کار و به ازای هر الگو ابتدا هدف الگو، بررسی یکی دو مثال و … بررسی میگردد و سپس به سمت پیاده سازی میریم و در آخر الگوهای مرتبط با آن را معرفی میکنیم پس قهوه یا چایی رو برای خودتون بیارید و کمربندها رو ببندید که راننده آماده رفتن به سمت پارت جدید یعنی بخش Abstract Factory هست🤓

هدف:

این الگو واسطی را ارائه میدهد برای ایجاد دسته ای از اشیاء مرتبط یا وابسته بدون مشخص کردن (ظاهر کردن/ نمایش دادن) کلاس های concrete (پیاده سازی شده) مریوطه.

نام دیگر الگو:

Kit

طرح مسئله

یک ابزار رابط کاربری را در نظر بگیرید که از چندین استاندارد عناصر بصری (Look) مثل رنگ ها، شکل ها، طرح ها و همچنین عناصر پویا (Feel) مثل دکمه ها، کادرها، منوها پشتیبانی میکند (در کتاب این موارد به عنوان استانداردهای look-and-feel آورده شده است که در ادامه با همین عنوان توضیحات ها را ادامه خواهم داد). look-and-feel مختلف ظاهر و رفتارهای متفاوتی را برای ویجت های (ویجت را یه واسط بین کاربر و برنامه در نظر بگیرید) رابط کاربری مانند نوارهای اسکرول، پنجره ها و دکمه ها تعریف می کنند. برای reusable بودن این ویجت ها یک برنامه کاربردی نباید خود را مختص یک look-and-feel خاص کند. ایجاد کلاس‌های look-and-feel خاص از ویجت‌ها در سراسر برنامه، تغییرات را در آینده دشوار خواهد کرد.

ما می‌توانیم این مشکل را با تعریف یک کلاس abstract با نام WidgetFactory که یک رابط را برای ایجاد هر نوع کلاسی که از ویجت ها استفاده میکنند حل کنیم همچنین یک کلاس abstract  برای هر نوع ویجت وجود دارد، و زیر کلاس‌های فرزند ( که concrete شناخته می شوند) ویجت‌ها را برای استانداردهای look-and-feel خاص پیاده‌سازی می‌کنند. رابط WidgetFactory دارای متدهایی است که برای هر کلاس از جنس انتزاعی widget یک widget object جدید برمی گرداند. مصرف کننده های این کلاس ، client ها هستند که یک متد را برای به دست آوردن نمونه های ویجت فراخوانی می کنند اما client ها از concrete class هایی که استفاده می کنند آگاه نیستند. بنابراین client ها ایزوله از Feel و Look پایه ای یا اصلی می مانند.

یک subclass مشخص از WidgetFactory برای هر استاندارد look-and-feel وجود دارد. هر subclass متدی را برای ایجاد ویجت مناسب برای look و feel اجرا می کند.

به عنوان مثال، متد CreateScrollBar در MotifWidgetFactory یک Motif scroll bar را نمونه‌سازی و بر می‌گرداند ( در اواخر دهه 1980 الی 1990 که شاید نهایت من 3 یا 4 سال داشتم🫡🤭، Motif یک ابزار محبوب GUI بود که برای X Window System و به صورت گسترده در سیستم های یونیکس مورد استفاده قرار می گرفت)، در حالی که متد مربوطه در PMWidgetFactory یک scroll bar را برای Presentation Manager یا PM را برمی‌گرداند (PM یک رابط گرافیکی کاربر برای سیستم عامل OS/2 بود که توسط IBM و مایکروسافت توسعه یافته بودند). Client ها ویجت ها را صرفاً از طریق رابط WidgetFactory ایجاد می کنند و هیچ دانشی از کلاس هایی که ویجت ها را برای look و feel خاصی پیاده سازی می کنند ندارند. به عبارت دیگر Client ها فقط باید به یک رابط تعریف شده توسط یک کلاس انتزاعی و نه یک کلاس مشخص خاص استفاده نمایند.

یک WidgetFactory همچنین وابستگی‌های بین concrete widget class ها را اعمال می‌کند. یک Motif scroll bar باید با یک Motif button و یک ویرایشگر Motif text editor استفاده شود و این محدودیت به‌طور خودکار در نتیجه استفاده از MotifWidgetFactory اعمال می‌شود.

کاربرد

زمانی از الگوی Abstract Factory استفاده کنید:

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

شرکت کنندگان  (اجزای این الگو)

‏AbstractFactory (WidgetFactory)‏:

یک رابط که با کمک متدهایی و در سطح انتزاع اشیاء مورد نظر را ایجاد و بر می گرداند.

‏ConcreteFactory (MotifWidgetFactory، PMWidgetFactory)‏:

پیاده سازی AbstractFactory (برای اشیاء مدنظر) در این سطح انجام میگردد.

AbstractProduct (Window, ScrollBar):

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

‏ConcreteProduct (MotifWindow، MotifScrollBar)‏:

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

Client:

‏موجودیتی که تنها اینترفیس های AbstractProduct و AbstractFactory را دسترسی (دید) دارد و از آنها استفاده می کند.

همکاری ها

به طور معمول یک single instance از کلاس ConcreteFactory در run-time ایجاد می شود. این ConcreteFactory اشیاء مورد نظر را با اجرای خاصی ایجاد می کند. برای ایجاد اشیاء مختلف، client ها باید از یک ConcreteFactory متفاوت استفاده کنند. ‏Abstract Factory ایجاد اشیاء مورد درخواست را به زیر کلاس ConcreteFactory خود موکول می کند (نگران نباشید در مثال عملی بهتر متوجه میشید😉)

مزایا و معایب

الگوی Abstract Factory دارای مزایا و معایب زیر است:

  1. concrete class ها را از هم ایزوله میکند. الگوی Abstract Factory به شما کمک میکند تا اشیایی که در برنامه شما ایجاد میشود را کنترل کنید چرا که یک factory مسئولیت encapsulate و فرآیند ایجاد یک object را بر عهده می گیرد و همچنین این موضوع برای client ها در زمان پیاده سازی کلاس ها ایزوله می باشد و در اصل client ها نمونه ها را از طریق abstract interface های خود دستکاری می کنند. نام کلاس های اشیاء مورد درخواست از پیاده سازی های ConcreteFactory ایزوله هستند و در کد code ظاهر نمی شوند.
  2. جابه جایی آبجکت های مورد انتظار را آسان می کند. یک کلاس concrete factory فقط یک بار در یک برنامه ظاهر می شود یعنی جایی که نمونه سازی می شود. این امر تغییر concrete factory را آسان می کند که یک برنامه کاربردی استفاده می کند و می تواند به سادگی با تغییر concrete factory از پیکربندی های مختلف اشیاء مورد انتظار استفاده کند. از آنجا که یک abstract factory یک خانواده کامل از اشیاء مورد نظر را ایجاد می کند، کل خانواده اشیاء مورد نظر به یکباره تغییر می کند. در مثال رابط کاربری ما، می‌توانیم از ویجت‌های Motif به ویجت‌های Presentation Manager به سادگی با تغییر آبجکت‌هایfactory مربوطه و ایجاد مجدد رابط، تغییر کاربری دهیم.
  3. باعث ایجاد ثبات در بین اشیاء مورد انتظار می شود. هنگامی که آبجکت های مورد انتظار در یک خانواده طوری طراحی شده اند که با هم کار کنند، مهم است که یک برنامه از اشیاء تنها یک خانواده در یک زمان استفاده کند. AbstractFactory اجرای این کار را آسان می کند.
  4. حمایت از انواع جدید آجکت های مورد نظر آسان نیست. توسعه AbstractFactory های مختلف برای تولید انواع جدید اشیاء آسان نیست. دلیل این امر این است که اینترفیس AbstractFactory مجموعه اشیایی را که می توان ایجاد کرد، اصلاح می کند. پشتیبانی از انواع جدید اشیاء مستلزم توسعه AbstractFactory است که شامل تغییر کلاس AbstractFactory و همه subclasse های آن است. ما یک راه حل برای این مشکل را در بخش پیاده سازی مورد بحث قرار می دهیم.

پیاده سازی

در اینجا چند تکنیک مفید برای اجرای الگوی Abstract Factory آورده شده است:

  1. Factory به عنوان singleton. یک برنامه معمولاً تنها به یک نمونه از ConcreteFactory برای هر خانواده از اشیاء مورد انتظار نیاز دارد. بنابراین معمولاً بهتر است به صورت Singleton پیاده سازی شود (این الگو رو بعدا صحبت میکنیم 😎) .
  2. ایجاد آیجکت های مورد انتظار. AbstractFactory فقط یک رابط برای ایجاد آبجکت های مد نظر را اعلام می کند. این به subclass های ConcreteProduct بستگی دارد که آنها را واقعاً ایجاد کند. متداول ترین راه برای انجام این کار، تعریف یک factory method است برای هر آبجکت (این الگو رو بعدا صحبت میکنیم 😎). concrete factory آبجکت ها خود را با پیاده سازی factory method به ازای هر آبجکت انجام می دهد در حالی که این پیاده سازی ساده است، نیاز به یک concrete factory subclass جدید برای هر خانواده از آبجکت ها دارد، حتی اگر خانواده آبجکت ها فقط کمی متفاوت باشد. اگر بسیاری از خانواده های اشیا مورد نظر موجود باشد،concrete factory را می توان با استفاده از الگوی Prototype پیاده سازی کرد. concrete factory با یک نمونه اولیه از هر شی در خانواده مقداردهی اولیه می شود و با شبیه سازی نمونه اولیه آن، شی جدیدی ایجاد می کند. رویکرد مبتنی بر Prototype-based نیاز به یک کلاس concrete factory جدید برای هر خانواده آبجکت جدید را از بین می برد.
  3. تعریف factory های توسعه پذیر. AbstractFactory معمولاً برای هر نوع آبجکتی که می تواند تولید شود یک operation تعریف می کند. انواع آبجکت ها با signature های مختلف در operation های تعریف شده مشخص می شوند . افزودن نوع جدیدی از اشیاء مستلزم تغییر اینترفیس AbstractFactory و تمام کلاس‌هایی است که به آن وابسته هستند.

یک طراحی انعطاف‌پذیرتر اما کمتر ایمن، افزودن یک پارامتر به operation هایی است که اشیا را ایجاد می‌کند. این پارامتر نوع شی ای که باید ایجاد شود را مشخص می کند. این می تواند یک شناسه کلاس، یک عدد صحیح، یک رشته یا هر چیز دیگری باشد که نوع آبجکت را مشخص می کند. در واقع با این رویکرد AbstractFactory تنها به یک “Make” با پارامتر های متفاوت در متد مربوطه نیاز دارد که نوع شی را ایجاد کند. این تکنیکی است که در Prototype و class-based abstract factory که قبلاً مورد بحث قرار گرفت استفاده می شود.

اما حتی زمانی که هیچ اجباری مورد نیاز نیست، یک مشکل ذاتی باقی می‌ماند: همه اشیاء به client یک abstract interface مشابه به عنوان یک مقدار بر میگردانند. client نمی تواند در مورد کلاس یک آبجکت متمایز یا فرضیات مطمئنی داشته باشد. اگر client ها نیاز به انجام operation ویژه ای برای subclass خاص داشته باشند، از طریق abstract interface قابل دسترسی نخواهند بود. اگرچه client می تواند یک downcast انجام دهد، این همیشه امکان پذیر یا ایمن نیست، زیرا downcast ممکن است با شکست مواجه شود. این trade-off کلاسیک برای یک رابط بسیار انعطاف پذیر و توسعه پذیر است.

کد نمونه

ما الگوی Abstract Factory را برای ایجاد مارپیچ هایی که در ابتدای این فصل بحث کردیم، اعمال می کنیم.

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

public class MazeFactory
{
    public virtual Maze MakeMaze()
    {
        return new Maze();
    }

    public virtual Wall MakeWall()
    {
        return new Wall();
    }

    public virtual Room MakeRoom(int roomNo)
    {
        return new Room(roomNo);
    }

    public virtual Door MakeDoor(Room r1, Room r2)
    {
        return new Door(r1, r2);
    }
}

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

در اینجا نسخه ای از CreateMaze وجود دارد که با در نظر گرفتن پارامتر MazeFactory این نقص را برطرف می کند:

public class MazeGame
{
    public Maze CreateMaze(MazeFactory factory)
    {
        Maze aMaze = factory.MakeMaze();
        Room r1 = factory.MakeRoom(1);
        Room r2 = factory.MakeRoom(2);
        Door aDoor = factory.MakeDoor(r1, r2);

        aMaze.AddRoom(r1);
        aMaze.AddRoom(r2);

        r1.SetSide(Direction.North, factory.MakeWall());
        r1.SetSide(Direction.East, aDoor);
        r1.SetSide(Direction.South, factory.MakeWall());
        r1.SetSide(Direction.West, factory.MakeWall());

        r2.SetSide(Direction.North, factory.MakeWall());
        r2.SetSide(Direction.East, factory.MakeWall());
        r2.SetSide(Direction.South, factory.MakeWall());
        r2.SetSide(Direction.West, aDoor);

        return aMaze;
    }
}

به طور مثال می‌توانیم EnchantedMazeFactory (که در پست قبل اشاره شد)، یک factory برای مارپیچ ‌های مسحور شده با زیر کلاس‌بندی MazeFactory ایجاد کنیم. EnchantedMazeFactory توابع اعضای مختلف را override می کند و زیر کلاس های مختلف اتاق، دیوار و غیره را برمی گرداند.

public class EnchantedMazeFactory : MazeFactory
{
    public EnchantedMazeFactory() { }

    public override Room MakeRoom(int n)
    {
        return new EnchantedRoom(n, CastSpell());
    }

    public override Door MakeDoor(Room r1, Room r2)
    {
        return new DoorNeedingSpell(r1, r2);
    }

    protected Spell CastSpell()
    {
        // Implementation of CastSpell
        return new Spell();
    }
}

public class EnchantedRoom : Room
{
    public EnchantedRoom(int n, Spell spell) : base(n) { }
}

public class DoorNeedingSpell : Door
{
    public DoorNeedingSpell(Room r1, Room r2) : base(r1, r2) { }
}

public class Spell { }

حالا فرض کنید می خواهیم یک بازی پیچ و خم بسازیم که در آن اتاق بتوانیم یک بمب قرار دهیم. اگر بمب منفجر شود، به دیوارها آسیب می رساند (حداقل). ما می‌توانیم یک subclass از Room بسازیم که ردیابی کند که آیا اتاق دارای بمب است و آیا بمب منفجر شده است یا خیر. همچنین برای پیگیری آسیب های وارد شده به دیوار به یک subclass دیوار نیاز داریم. ما این کلاس ها را RoomWithABomb و BombedWall می نامیم.

آخرین کلاسی که ما تعریف خواهیم کرد BombedMazeFactory است، یک زیر کلاس از MazeFactory است که تضمین می کند دیوارها از کلاس BombedWall و اتاق ها از کلاس RoomWithABomb هستند. BombedMazeFactory فقط باید دو تابع را override  کند:

public class BombedMazeFactory : MazeFactory
{
    public override Wall MakeWall()
    {
        return new BombedWall();
    }

    public override Room MakeRoom(int n)
    {
        return new RoomWithABomb(n);
    }
}

برای ساختن MazeGame ساده که می تواند حاوی بمب باشد، ما به سادگی CreateMaze را با یک BombedMazeFactory فراخوانی می کنیم:

var game = new MazeGame();
var factory = new BombedMazeFactory();
var maze = game.CreateMaze(factory);

CreateMaze می تواند یک نمونه از EnchantedMazeFactory را به همان خوبی برای ساخت مارپیچ های مسحور شده استفاده کند.

توجه داشته باشید که MazeFactory فقط مجموعه ای از factory method ها است. این رایج ترین روش برای اجرای الگوی Abstract Factory است. همچنین توجه داشته باشید که MazeFactory یک کلاسabstract  نیست بنابراین هم به عنوان AbstractFactory و هم به عنوان ConcreteFactory عمل می کند. این یکی دیگر از پیاده سازی های رایج برای کاربردهای ساده الگوی Abstract Factory است. از آنجایی که Maze Factory یک کلاس concrete است که کاملاً از factory method ها تشکیل شده است ساختن MazeFactory جدید با ایجاد یک زیر کلاس و override کردن متدی که نیاز به تغییر دارند آسان است.

‏CreateMaze از متد SetSide در اتاق ها برای مشخص کردن طرف آنها استفاده کرد. اگر اتاق هایی با BombedMazeFactory ایجاد کند، مارپیچی از اشیاء RoomWithABomb با دو طرف BombedWall تشکیل می شود. اگر RoomWithABomb مجبور بود به یک عضو مخصوص زیر کلاس BombedWall دسترسی داشته باشد، باید cast به reference های wall داشته باشد (BombedWall : Wall).

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

public enum Direction { North, South, East, West }


public abstract class MapSite
{
    public abstract void Enter();
}



public class Room : MapSite
{
    private MapSite[] _sides = new MapSite[4];
    private int _roomNumber;

    public Room(int roomNo)
    {
        _roomNumber = roomNo;
    }

    public MapSite GetSide(Direction direction)
    {
        return _sides[(int)direction];
    }

    public void SetSide(Direction direction, MapSite mapSite)
    {
        _sides[(int)direction] = mapSite;
    }

    public override void Enter()
    {
        // Implementation for entering the room
    }
}


public class Wall : MapSite
{
    public Wall() { }

    public override void Enter() { }
}

public class Door : MapSite
{
    private Room _room1;
    private Room _room2;
    private bool _isOpen;

    public Door(Room room1 = null, Room room2 = null)
    {
        _room1 = room1;
        _room2 = room2;
        _isOpen = false;
    }

    public override void Enter() { }

    public Room OtherSideFrom(Room room)
    {
        return room == _room1 ? _room2 : _room1;
    }
}

public class Maze
{
    private List<Room> rooms;

    public Maze()
    {
        rooms = new List<Room>();
    }

    public void AddRoom(Room room)
    {
        rooms.Add(room);
    }

    public Room RoomNo(int roomNumber)
    {
        return rooms[roomNumber];
    }
}

public class MazeFactory
{
    public virtual Maze MakeMaze()
    {
        return new Maze();
    }

    public virtual Wall MakeWall()
    {
        return new Wall();
    }

    public virtual Room MakeRoom(int roomNo)
    {
        return new Room(roomNo);
    }

    public virtual Door MakeDoor(Room r1, Room r2)
    {
        return new Door(r1, r2);
    }
}
public class MazeGame
{
    public Maze CreateMaze(MazeFactory factory)
    {
        Maze aMaze = factory.MakeMaze();
        Room r1 = factory.MakeRoom(1);
        Room r2 = factory.MakeRoom(2);
        Door aDoor = factory.MakeDoor(r1, r2);

        aMaze.AddRoom(r1);
        aMaze.AddRoom(r2);

        r1.SetSide(Direction.North, factory.MakeWall());
        r1.SetSide(Direction.East, aDoor);
        r1.SetSide(Direction.South, factory.MakeWall());
        r1.SetSide(Direction.West, factory.MakeWall());

        r2.SetSide(Direction.North, factory.MakeWall());
        r2.SetSide(Direction.East, factory.MakeWall());
        r2.SetSide(Direction.South, factory.MakeWall());
        r2.SetSide(Direction.West, aDoor);

        return aMaze;
    }
}

public class BombedMazeFactory : MazeFactory
{
    public override Wall MakeWall()
    {
        return new BombedWall();
    }

    public override Room MakeRoom(int n)
    {
        return new RoomWithABomb(n);
    }
}

public class BombedWall : Wall { }

public class RoomWithABomb : Room
{
    public RoomWithABomb(int n) : base(n) { }
}

الگوهای مرتبط

AbstractFactory کلاس ها اغلب با Factory Method پیاده سازی می شوند اما می توانند با استفاده از Prototype نیز پیاده سازی شوند. یک concrete factory نیز اغلب یک singleton است.

 

خوب اگر موافق باشید سفر ما تا اینجا باشه و بزنیم کنار تا شما یه استراحت بکنید و در قسمت بعد وارد دومین الگو یعنی Builder بشیم😉

مرسی که تا اینجا با من همراه بودید راستی نظرات یادتون نره🫡

 

اگر مطالب تا اینجا به دردتون خورده خوشحال میشم با کلیک روی تصویر زیر برای من یه قهوه خوشمزه بگیرید تا مغزم حسابی کیف کنه 😍!!!

دسته بندی شده در: