خوب خوب خوب بعد🫡! بعد یه مدت غیبت و مریضی و … من آماده ام که Factory method فریاد کنم😁 خوب حالا که انگار آماده اید قهوه یا چایی رو برای خودتون بیارید و کمربندها رو ببندید که راننده آماده رفتن به سمت پارت جدید یعنی بخش Factory Method هست🤓
هدف:
ایجاد یک interface برای ایجاد یک object، اما اجازه میدهد subclass ها تصمیم بگیرند که کدام کلاس را نمونه سازی کنند. Factory Method به یک کلاس اجازه می دهد نمونه سازی را به subclass ها موکول کند.
نام دیگر الگو:
Virtual Constructor
طرح مسئله:
همانطور که می دانید Framework ها از کلاس های abstract برای تعریف و حفظ روابط بین اشیا استفاده می کنند. یک Framework اغلب مسئول ایجاد این اشیا نیز می باشد.
حالا فرض کنید یک framework داریم که می تواند چندین مدل document را به کاربر ارائه دهد. دو انتزاع کلیدی در این framework کلاسهای Application و Document هستند. هر دو کلاس انتزاعی هستند و client ها باید آنها را به subclass های مشخص تقسیم کنند تا پیادهسازیهای خاص برنامه خود را درک کنند. برای ایجاد یک برنامه طراحی به عنوان مثال، کلاس های DrawingApplication و DrawingDocument را تعریف می کنیم. کلاس Application مسئول مدیریت اسناد است و آنها را در صورت نیاز ایجاد می کند مثلاً زمانی که کاربر Open یا New را از یک منو انتخاب کند.
از آنجایی که زیر کلاس Document خاص که باید نمونه سازی شود مختص برنامه کاربردی است، کلاس Application نمی تواند زیر کلاس Document را برای نمونه سازی کلاس Application پیش بینی کند، فقط می داند که یک سند جدید چه زمانی “When” باید ایجاد شود، نه اینکه چه نوع سندی “what kind” را باید ایجاد کند. این یک دوراهی ایجاد میکند: framework باید کلاسها را نمونهسازی کند، اما فقط کلاسهای انتزاعی را میداند که میتواند نمونهسازی کند.
الگوی Factory Method یک راه حل ارائه می دهد. دانش (knowledge) مربوط به اینکه کدام زیر کلاس سند می بایست ایجاد شود را کپسوله می کند و این دانش را از framework خارج می کند.
Subclass های برنامه متدهای abstract CreateDocument را در Application دوباره تعریف می کنند تا زیر کلاس Document مناسب را برگردانند. هنگامی که یک زیر کلاس Application نمونه سازی شد، سپس می تواند اسناد خاص برنامه را بدون اطلاع از کلاس آنها نمونه سازی کند. ما CreateDocument را یک factory method می نامیم زیرا مسئول “تولید-ساخت” یک شی است.
کاربرد
زمانی از الگوی Factory Method استفاده کنید:
- یک کلاس نمی تواند کلاس اشیایی را که باید ایجاد کند، پیش بینی کند.
- یک کلاس می خواهد زیر کلاس هایش اشیایی را که می سازد مشخص کنند.
- کلاسها مسئولیت را به یکی از چندین زیر کلاس کمکی محول میکنند، و شما میخواهید دانش مربوط به اینکه کدام زیرکلاس کمکی نماینده است را بومیسازی کنید.
ساختار
شرکت کنندگان ( اجزای این الگو)
Product (Document)
رابط اشیا را که factory method ایجاد می کند تعریف می کند.
ConcreteProduct (MyDocument)
رابط اشیائی که ایجاد می شوند را پیاده سازی می کند.
Creator (Application)
factory method را تعریف می کند که یک شی از نوع Product (اشیاء مورد انتظار) را برمی گرداند. Creator همچنین ممکن است یک پیاده سازی پیش فرض از factory method را تعریف کند که یک شی ConcreteProduct پیش فرض را برمی گرداند.
ممکن است factory method را برای ایجاد یک شیء Product فراخوانی کند.
ConcreteCreator (MyApplication)
factory method را برای برگرداندن نمونه ای از ConcreteProduct به عبارتی override می کند.
همکاری ها
Creator برای تعریف factory method به subclass های خود متکی است تا نمونه ای از ConcreteProduct مناسب را برگرداند.
مزایا و معایب
Factory method ها نیاز به bind شدن کلاس های خاص برنامه به کد شما را، حذف می کنند. کد فقط با interface شی ایجاد شده توسط Factory method (محصول) سروکار دارد. بنابراین می تواند با هر کلاس ConcreteProduct تعریف شده توسط کاربر کار کند.
یک نقطه ضعف بالقوه factory method ها این است که client ها ممکن است مجبور شوند کلاس Creator را فقط برای ایجاد یک شی ConcreteProduct خاص زیر کلاسبندی کنند. وقتی client مجبور است کلاس Creator را به هر حال زیر کلاس بندی کند خوب است، اما در غیر این صورت مشتری اکنون باید با نقطه تکامل دیگری سر و کار داشته باشد.
در اینجا دو نکته اضافی از الگوی Factory method وجود دارد:
- hook برای subclass ها فراهم می کند. ایجاد اشیاء در داخل یک کلاس با factory method همیشه انعطاف پذیرتر از ایجاد مستقیم یک شی است. Factory Method به subclass ها یکhook برای ارائه یک نسخه توسعه یافته از یک شی می دهد.
در مثال Document، کلاس Document می تواند یک factory method به نام CreateFileDialog تعریف کند که یک شی file dialog پیش فرض برای باز کردن یک سند موجود ایجاد می کند. یک زیرکلاس Document میتواند با override این factory method یک file dialog خاص برنامه را تعریف کند. در این مورد factory method انتزاعی نیست اما یک پیاده سازی پیش فرض معقول ارائه می دهد.
- سلسله مراتب کلاس های موازی را به هم متصل می کند (parallel class hierarchies). در مثال هایی که تاکنون در نظر گرفتیم factory method فقط توسط Creator ها فراخوانی می شود اما لازم نیست که اینطور باشد. Client ها می توانند factory method ها را به صورت ساده تری استفاده کنند به خصوص در مورد سلسله مراتب کلاس های موازی.
سلسله مراتب کلاس های موازی زمانی حاصل می شود که یک کلاس برخی از مسئولیت های خود را به یک کلاس جداگانه واگذار کند. شکل های گرافیکی را در نظر بگیرید که می توانند به صورت تعاملی دستکاری شوند یعنی می توان آنها را با استفاده از موس کشیده، حرکت داد یا چرخاند. اجرای چنین تعاملاتی همیشه آسان نیست. اغلب نیاز به ذخیره و به روز رسانی اطلاعاتی دارد که وضعیت دستکاری را در یک زمان معین ثبت می کند. این حالت فقط در هنگام دستکاری مورد نیاز است. بنابراین نیازی به نگهداری آن در شی شکل نیست. علاوه بر این، چهره های مختلف زمانی که کاربر آنها را دستکاری می کند رفتار متفاوتی دارند. به عنوان مثال، جابه جایی یک شکل خط ممکن است اثر جابجایی روی یک نقطه پایانی داشته باشد، در حالی که جابه جایی یک شکل متنی ممکن است فاصله خطوط آن را تغییر دهد.
با این محدودیتها، بهتر است از یک شی Manipulator جداگانه استفاده کنید که تعامل را پیادهسازی میکند و هر حالت خاص تغییرات مورد نیاز را پیگیری میکند. حالت های مختلف از زیر کلاس های Manipulator مختلف برای رسیدگی به تعاملات خاص استفاده می کنند. سلسله مراتب کلاس Manipulator به دست آمده (حداقل تا حدی) سلسله مراتب کلاس Figure را موازی می کند.
کلاس Figure یک factory method را با نامCreateManipulator ارائه میکند که به client ها اجازه میدهد یک Manipulator مربوط به شکل را ایجاد کنند. Figure subclass ها این متد راoverride میکنند تا نمونهای از زیرکلاس Manipulator را که برای آنها مناسب است برگردانند. از طرف دیگر، کلاس Figure ممکن است CreateManipulator را برای برگرداندن یک نمونه Manipulator پیش فرض پیاده سازی کند و زیر کلاس های Figure ممکن است به سادگی آن پیش فرض را به ارث ببرند. کلاسهای Figure که این کار را انجام میدهند نیازی به زیر کلاس Manipulator مربوطه ندارند، از این رو سلسله مراتب فقط تا حدی موازی هستند.
توجه کنید که factory method چگونه ارتباط بین دو سلسله مراتب کلاس را تعریف می کند. این دانش را بومی سازی می کند که کدام کلاس ها به هم تعلق دارند.
پیاده سازی
هنگام اعمال الگوی Factory Method موارد زیر را در نظر بگیرید:
- Two major varieties. دو نوع اصلی الگوی Factory Method عبارتند از: (1) موردی که کلاس Creator یک abstract class است و پیاده سازی برای factory method که اعلام می کند ارائه نمی کند، و (2) موردی که Creator یک کلاس concrete است و یک پیاده سازی پیش فرض برای factory method ارائه می دهد. همچنین ممکن است یک abstract class داشته باشیم که پیاده سازی پیش فرض را تعریف کند، اما این کمتر رایج است.
مورد اول به زیر کلاسها نیاز دارد تا پیادهسازی را تعریف کنند، زیرا هیچ پیشفرض معقولی وجود ندارد. این معضل نیاز به نمونه سازی کلاس های غیرقابل پیش بینی را دور می زند. در حالت دوم، concrete Creator از factory method در درجه اول برای انعطاف پذیری استفاده می کند. این از قانونی پیروی می کند که می گوید: “اشیاء را در یک عملیات جداگانه ایجاد کنید تا subclass ها بتوانند نحوه ایجاد آنها را override کنند.” این قانون تضمین میکند که طراحان subclass ها میتوانند کلاس اشیایی را که کلاس والدشان نمونهسازی میکند در صورت لزوم تغییر دهند.
- Parameterized factory methods. تغییر دیگری در الگو factory method اجازه می دهد تا انواع مختلفی از اشیاء را ایجاد کند. factory method پارامتری را می گیرد که نوع شی را که باید ایجاد شود مشخص می کند. تمام اشیایی که factory method ایجاد می کند رابط شی ایجاد شده را به اشتراک خواهند گذاشت. در مثال Document، Application ممکن است از انواع مختلفی از اسناد پشتیبانی کند. برای تعیین نوع سند برای ایجاد، یک پارامتر اضافی به CreateDocument میدهید.
یک parameterized factory method شکل کلی زیر را دارد که در آن MyProduct و YourProduct زیر کلاس های محصول هستند:
public abstract class Creator { public abstract Product Create(ProductId id); } public class MyCreator : Creator { public override Product Create(ProductId id) { if (id == ProductId.YOURS) return new MyProduct(); if (id == ProductId.MINE) return new YourProduct(); return base.Create(id); // called if all others fail } }
Override کردن parameterized factory method به شما این امکان را می دهد که به راحتی و انتخابی اشیایی را که یک Creator تولید می کند گسترش یا تغییر دهید. میتوانید شناسههای جدیدی را برای انواع جدید اشیاء معرفی کنید یا میتوانید شناسههای موجود را با اشیاء مختلف مرتبط کنید.
به عنوان مثال، یک subclass برای MyCreator می تواند MyProduct و YourProduct را swap کند و از یک subclass جدید TheirProduct پشتیبانی کند:
public abstract class Creator { public abstract Product Create(ProductId id); } public class MyCreator : Creator { public override Product Create(ProductId id) { if (id == ProductId.YOURS) return new MyProduct(); if (id == ProductId.MINE) return new YourProduct(); if (id == ProductId.THEIRS) return new TheirProduct(); return base.Create(id); // called if all others fail } }
- انواع و مسائل خاص زبان. زبان های مختلف خود را به تغییرات و اخطارهای متفاوت دیگر وا می دارند.
یک رویکرد انعطافپذیرتر شبیه به parameterized factory method، ذخیره کلاسی است که قرار است بهعنوان متغیر کلاس Application ایجاد شود. به این ترتیب شما مجبور نیستید Application را برای تغییر اشیاء مدنظر طبقه بندی کنید.
نکته: شما میتوانید در زمان ایجاد شی ابتدا بررسی کنید که آن شی وجود داشته باشد و اگر وجود نداشته باشد آن را ایجاد می کند. این تکنیک گاهی اوقات به نام Lazy Initialization نامیده می شود. کد زیر نمونه ای از آن را نشان می دهد:
public class CreatorWithGetProduct { private Product _product; public Product GetProduct() { if (_product == null) { _product = CreateProduct(); } return _product; } protected virtual Product CreateProduct() { return null; // Implementation should be provided in derived classes } }
- استفاده از قالب ها(در زبان های امروزی آن را به عنوان Generic میشناسیم) برای جلوگیری از subclassing. همانطور که اشاره کردیم، یکی دیگر از مشکلات بالقوه factory method ها این است که ممکن است فقط برای ایجاد اشیاء مناسب مورد انتظار که قرار است ساخته شود شما را مجبور به ایجاد subclass کند. راه دیگر برای دور زدن این موضوع در زبان هایی مثل C++، C# و … ارائه یک template subclass یا generic subclass از Creator است که با کلاس Product پارامترگذاری می شود.
با استفاده از این template، client فقط کلاس product را ارائه میکند بدون اینکه subclass برای Creator لازم باشد.
public abstract class CreatorBase { public abstract Product CreateProduct(); } public class StandardCreator<TheProduct> : CreatorBase where TheProduct : Product, new() { public override Product CreateProduct() { return new TheProduct(); } }
- قراردادهای نامگذاری. استفاده از قراردادهای نامگذاری که مشخص می کند از factory method استفاده می کنید، تمرین خوبی است که می بایست آن را در نظر بگیریم.
کد نمونه
تابع CreateMaze یک maze می سازد و برمی گرداند. یکی از مشکلات این عملکرد این است که کلاسهای maze، اتاقها، درها و دیوارها را hard-code میکند. ما factory method را معرفی می کنیم تا به subclass اجازه دهیم این اجزا را انتخاب کنند.
ابتدا factory method را در MazeGame برای ایجاد maze، اتاق، دیوار، اشیاء و در، تعریف می کنیم.
هر factory method یک maze component از یک نوع معین را برمی گرداند. MazeGame پیاده سازی های پیش فرضی را ارائه می دهد که ساده ترین انواع maze، اتاق ها، دیوارها و درها را برمی گرداند.
اکنون میتوانیم CreateMaze را برای استفاده از factory method بازنویسی کنیم:
public class MazeGame { public virtual Maze CreateMaze() { Maze aMaze = MakeMaze(); Room r1 = MakeRoom(1); Room r2 = MakeRoom(2); Door theDoor = MakeDoor(r1, r2); aMaze.AddRoom(r1); aMaze.AddRoom(r2); r1.SetSide(Direction.North, MakeWall()); r1.SetSide(Direction.East, theDoor); r1.SetSide(Direction.South, MakeWall()); r1.SetSide(Direction.West, MakeWall()); r2.SetSide(Direction.North, MakeWall()); r2.SetSide(Direction.East, MakeWall()); r2.SetSide(Direction.South, MakeWall()); r2.SetSide(Direction.West, theDoor); return aMaze; } // factory methods: protected virtual Maze MakeMaze() { return new Maze(); } protected virtual Room MakeRoom(int n) { return new Room(n); } protected virtual Wall MakeWall() { return new Wall(); } protected virtual Door MakeDoor(Room r1, Room r2) { return new Door(r1, r2); } }
بازیهای مختلف میتوانند subclass مربوط به MazeGame را برای بخش های مختلف از maze سفارشی کنند. Subclass های مربوط به MazeGame میتوانند برخی یا همه factory method ها را برای تعیین تغییرات در محصولات Override کنند. به عنوان مثال، یک BombedMazeGame می تواند محصولات اتاق و دیوار را دوباره تعریف کند تا انواع بمب را بازگرداند:
public class BombedMazeGame : MazeGame { public BombedMazeGame() { } protected override Wall MakeWall() { return new BombedWall(); } protected override Room MakeRoom(int n) { return new RoomWithABomb(n); } }
یک نوع EnchantedMazeGame ممکن است به این صورت تعریف شود:
public class EnchantedMazeGame : MazeGame { public EnchantedMazeGame() { } protected override Room MakeRoom(int n) { return new EnchantedRoom(n, CastSpell()); } protected override Door MakeDoor(Room r1, Room r2) { return new DoorNeedingSpell(r1, r2); } protected Spell CastSpell() { // Implementation of CastSpell return new Spell(); } }
و در نهایت کد پایانی ما به شکل زیر می باشد:
namespace FactoryMethod; 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.Count < roomNumber ? null : rooms[roomNumber-1]; } } public class MazeGame { public virtual Maze CreateMaze() { Maze aMaze = MakeMaze(); Room r1 = MakeRoom(1); Room r2 = MakeRoom(2); Door theDoor = MakeDoor(r1, r2); aMaze.AddRoom(r1); aMaze.AddRoom(r2); r1.SetSide(Direction.North, MakeWall()); r1.SetSide(Direction.East, theDoor); r1.SetSide(Direction.South, MakeWall()); r1.SetSide(Direction.West, MakeWall()); r2.SetSide(Direction.North, MakeWall()); r2.SetSide(Direction.East, MakeWall()); r2.SetSide(Direction.South, MakeWall()); r2.SetSide(Direction.West, theDoor); return aMaze; } // factory methods: protected virtual Maze MakeMaze() { return new Maze(); } protected virtual Room MakeRoom(int n) { return new Room(n); } protected virtual Wall MakeWall() { return new Wall(); } protected virtual Door MakeDoor(Room r1, Room r2) { return new Door(r1, r2); } } public class BombedWall : Wall { } public class RoomWithABomb : Room { public RoomWithABomb(int n) : base(n) { } } public class BombedMazeGame : MazeGame { public BombedMazeGame() { } protected override Wall MakeWall() { return new BombedWall(); } protected override Room MakeRoom(int n) { return new RoomWithABomb(n); } }
الگوهای مرتبط
Abstract Factory اغلب با factory method ها پیاده می شود. مثال Motivation در الگوی Abstract Factory، الگوی Factory Method را به خوبی نشان می دهد.
Factory method ها معمولاً در Template Method ها استفاده می کنند. در مثال document بالا، NewDocument یک Template method است.
خوب اگر موافق باشید سفر ما تا اینجا باشه و بزنیم کنار تا شما یه استراحت بکنید و در قسمت بعد وارد چهارمین الگو یعنی Prototype بشیم😉
مرسی که تا اینجا با من همراه بودید راستی نظرات یادتون نره🫡
اگر مطالب تا اینجا به دردتون خورده خوشحال میشم با کلیک روی تصویر زیر برای من یه قهوه خوشمزه بگیرید تا مغزم حسابی کیف کنه 😍!!!