آموزش دیزاین پترن Prototype در کاتلین
دیزاین پترن Prototype که همچنین با نام Clone (به معنای شبیه سازی) شناخته شده است یک دیزاین پترن Creational هست که بهت اجازه می ده تا یک شئ موجود رو بدون آنکه کاری کنی تا کدهایت به کلاس هایشان وابستگی داشته باشن، کپی کنی.
آموزش دیزاین پترن Prototype در کاتلین
چرا دیزاین پترن Prototype؟ فرض کن شئ ای داری و می خوای یک کپی دقیق ازش ایجاد کنی. چطور می خوای انجامش بدی؟ اول باید یک شئ جدید از همان کلاس ایجاد کنی. بعد باید تمام فیلد های شئ اصلی رو مرور کنی و مقادیر آنها رو در شئ جدید کپی کنی.
بسیار خب! اما یک مسئله دیگه. همۀ شئ ها رو نمی شه با این شیوه کپی کرد چون برخی از فیلد های شئ ممکنه خصوصی باشن و خارج از خود شئ قابل مشاهده نباشن.
یک مشکل دیگر هم در رویکرد مستقیم وجود داره.
از آنجایی که باید کلاس شئ رو برای ایجاد یک کپی از شئ اش بشناسی، کدت به آن کلاس وابسته می شه. اگر با بوجود اومدن وابستگی اضافی مشکلی نداری، یک مشکل دیگری وجود داره. گاهی اوقات تو فقط interface ای که شئ ها ازش پیروی می کنن رو می شناسی، اما concrete class هایش رو نمی شناسی(یعنی subclass هایش)؛ در این صورت برای مثال یک پارامتر در یک method هر شئ ای رو که از interface ای یکسان پیروی می کنه خواهد پذیرفت!
تعریف : یک کلاس concrete کلاسی است که می تواند نمونه سازی بشه، در مقابل کلاس های انتزاعی که نمی توانن.
راهکار برای کپی کردن دقیق یک شئ و جلوگیری از رخدادن چنین مشکلاتی دیزاین پترن Prototype است. دیزاین پترن Prototype فرآیند شبیه سازی رو به شئ های واقعی که در حال شبیه سازی هستند واگذار می کنه.
دیزاین پترن Prototype یک interface مشترک برای همۀ شئ هایی که از clone (به معنای شبیه سازی) پشتیبانی می کنن اعلام می کنه. این interface بهت این امکان رو می ده که یک شئ رو بدون متصل کردن کدت با کلاس آن شئ، شبیه سازی کنی. معمولا چنین interface ای شامل یک Method برای شبیه سازی خواهد بود با نام clone
.
شیوه پیاده سازی متد clone
در همۀ کلاس ها بسیار شبیه به هم هست. متد یک شئ از کلاس فعلی ایجاد می کنه و تمام مقادیر فیلدهای شئ قدیمی رو به شئ جدید منتقل می کنه. تو حتی می تونی فیلد های private رو کپی کنی چون اکثر زبان های برنامه نویسی از جمله کاتلین به شئ ها جازه میدن به فیلد های خصوصی دیگر شئ ها متعلق به همان کلاس دسترسی پیدا کنن.
شئ ای که از شبیه سازی شدن خودش، پشتیبانی می کنه Prototype نامیده می شه. هنگامی که شئ های شما ده ها فیلد و صد ها پیکربندی ممکن دارند شبیه سازی آنها ممکنه به عنوان جایگزینی بجای ایجاد subclass ها باشه.
اینجوری کار می کنه: تو مجموعه ای از شئ ها رو ایجاد می کنی که به روش های مختلف پیکر بندی شُدن. هنگامی که به یک شئ مانند آنچه که پیکربندی کردی نیاز داری، بجای ایجاد یک شئ از اول، فقط یک prototype رو شبیه سازی می کنی.
برای یادگیری تمام معماری های رایج در برنامه نویسی مدرن اندروید و تسلط به پیشرفته ترین و خفن ترین ابزار های برنامه نویسی اندروید دورۀ نخبگان اندروید استاد نوری رو ببین
قیاس کردن دیزاین پترن Prototype در جهان واقعی
در زندگی واقعی، Prototype ها برای انجام آزمایش های گوناگون قبل از آغاز به تولید یک محصول استفاده می شه. با این روی، در این مورد Prototype ها در هیچ تولید واقعی ای شرکت نمی کنن و بجایش نقش منفعل رو بازی می کنن.
از آنجایی که Prototype های صنعتی واقعا خود رو کپی نمی کنن، یک تشبیه بسیار نزدیک به دیزاین پترن Prototype فرآیند تقسیم سلولی میتوزی (در زیست شناسی) هست. پس از تقسیم میتوزی، یک جفت سلول یکسان تشکیل می شه. سلول اصلی به عنوان یک Prototype عمل می کنه و نقش فعالی در ایجاد کپی داره.
ساختار دیزاین پترن Prototype
میخوایم باهم پیاده سازی این الگوی طراحی یا دیزاین پترن رو بررسی کنیم.
پیاده سازی پایه دیزاین پترن Prototype
اگر اصلا نمیدونین UML چیه آموزش جامع UML رو در این مقاله مفصلی که نوشتم بخونین.
اینترفیس Prototype : دارندۀ تابع های شبیه سازی می باشه. در بیشتر موارد تنها یک تابع با نام clone داره.
کلاس concrete Prototype : تابع های شبیه سازی رو پیاده سازی می کنه. افزون بر کپی کردن داده های شئ اصلی در شبیه سازی، این تابع همچنین ممکنه برخی از موارد حاشیه ای فرآیند شبیه سازی، مربوط به شبیه سازی شئ های مرتبط، گشودن وابستگی های بازگشتی و… رو هم انجام بده.
Client : Client می تونه یک کپی از هر شئ که از اینترفیس Prototype پیروی می کنه تولید کنه.
پیاده سازی Prototype registry
راه آسانی برای دسترسی Prototype های پرکاربرد فراهم می کنه. مجموعه ای از شئ های از پیش ساخته شده رو که آمادۀ کپی هستند ذخیره می کنه. ساده ترین نمونۀ Prototype Registry یک هش مپ name → prototype
می باشه.
با این روی، اگر به معیار های جست و جوی بهتری نسبت به یک نام ساده نیاز داری، می تونی نسخۀ قوی تری از Registry بسازی.
مثال کد نویسی دیزاین پترن Prototype
در این مثال دیزاین پترن Prototype به بهت امکان می ده کپی های دقیقی از اجسام هندسی بسازی بدون آنکه کدت رو به کلاس هایشان متصل کنی.
همۀ کلاس های Shape از interface یکسان پیروی می کنن، که یک تابع شبیه سازی ارائه می کنه.
//Base Prototype. abstract class Shape { var x: Int = 0 var y: Int = 0 var color: String = "" //یک سازندۀ معمولی constructor() //سازندۀ پروتوتایپ. یک شئ جدید با استفاده از //مقدارهای شئ کنونی مقدار دهی می شه constructor(source: Shape) : this() { x = source.x y = source.y color = source.color } //عملیات شبیه سازی یکی از زیر کلاس های کلاس شکل رو بر می گردونه abstract fun clone() : Shape } //concrete prototype //متد شبیه سازی، در یک حرکت با صدا زدن سازندۀ کلاس فعلی //یک شئ جدید می سازه و شئ فعلی رو //به عنوان آرگمان سازندۀ کلاس پاس می ده. //انجام تمام کپی های واقعی در سازنده، یاری می کنه //نتیجه رو ثابت نگه داری : //تا زمانی که شئ جدید تماماً ساخته نشه، سازنده نتیجه رو بر نمی گردونه //بنابرین هیچ شئ نمی تونه به یک شبیه ساز نیمه ساخته شده اشاره داشته باشه class Rectangle : Shape { var width : Int = 0 var height : Int = 0 constructor() : super() constructor(source : Rectangle) : super(source){ //به سازندۀ کلاس والد برای کپی کردن //در فیلد های خصوصی کلاس والد نیازه width = source.width height = source.height } override fun clone(): Shape { return Rectangle(this) } } class Circle : Shape{ var radius : Int = 0 constructor() : super() constructor(source: Circle) : super(source){ radius = source.radius } override fun clone(): Shape { return Circle(this) } } //جایی در کد کلاینت class Application{ var shapes = mutableListOf<Shape>() init{ val circle = Circle() circle.x = 10 circle.y = 10 circle.radius = 20 shapes.add(circle) val anotherCircle = circle.clone() shapes.add(anotherCircle) //anotherCircle //یک کپی دقیق از شئ کلاس دایره که بالاش ساختیم، داره val rectangle = Rectangle() rectangle.width = 10 rectangle.height = 10 shapes.add(rectangle) } fun businessLogic(){ //پروتوتایپ به ما اجاره می ده بدون اینکه تایپ شئ ای رو //بدونیم ازش کپی درست کنیم val shapesCopy = mutableListOf<Shape>() //برای مثال ما عناصر دقیق لیست شکل ها رو نمی دونیم //تنها چیزی که ما می دونیم آنه که همشون شکل هستن //اما به کمک مفهوم چند ریختی که یکی از مفهوم های شئ گرایی هست //هنگامی که ما تابع کلون رو درون یک شکل صدا می زنیم //برنامه کلاس واقعی اش رو بررسی می کنه و تابع کلون //مناسب رو که در آن کلاس نعریف شده صدا می زنه //به همین دلیله که ما کلون های مناسب رو //بجای مجموعه ای از شئ های شکل ساده می گیریم shapes.forEach { shapesCopy.add(it.clone()) } //ShapesCopy //کپی های دقیقی از عنصر های //Shapes //داره shapesCopy.forEach { it.apply { println("x→$x\ny→$y\ncolor→$color\n") //چاپ کردن تمام شئ های کپی شده } } } } fun main() { val app = Application() app.businessLogic() //بیزنس لاجیک یعنی منطق برنامه }
دیزاین پترن Prototype در کجا قابلیت اجرایی دارد؟
- از دیزاین پترن Prototype زمانی استفاده کن که کدت نباید به کلاس های concrete شئ هایی که نیاز داری کپی کنی وابسته باشه
هنگامی که کدت با شئ هایی که از طریق کد های شخص ثالث به شما ارسال می شه کار می کنه، مسئله یادشده زیاد پیش میاد.
دیزاین پترن Prototype کد کلاینت رو با یک interface عمومی برای کار با همۀ شئ هایی که از شبیه سازی پشتیبانی می کنن فراهم می کنه.
- هنگامی که می خواهی تعداد subclass هایی رو که فقط در نحوۀ مقدار دهی اولیۀ شئ های مربوط به خود متفاوت هستن کاهش بدی از دیزاین پترن Prototype استفاده کن
فرض کن یک کلاس پیچیده داری که قبل از استفاده از آن نیاز به یک پیکربندی پر زحمت داره. چندین راه متداول برای پیکر بندی این کلاس وجود داره و این کد در برنامه ات پراکنده است.
برای کاهش تکرار چندین subclass ایجاد می کنی و هر کد پیکربندی رایج رو در سازندۀ آنها قرار می دی. تو مشکل تکرار رو حل کردی اما اکنون subclass های ساختگی زیادی داری.
دیزاین پترن Prorotype بهت امکان می ده از مجموعه ای از شئ های از پیش ساخته شده استفاده کنی که به روش های مختلف به عنوان Prototype اِ (نمونۀ اولیۀ) پیکربندی شده هستن.(معنای واژه Prototype یعنی نمونۀ اولیه)
بجای نمونه سازی(نمونه سازی در بحث شئ گرایی یعنی ایجاد یک شئ از کلاس، وقتی می گیم یک نمونه از یک کلاس داریم یعنی یک شئ از کلاس داریم) یک subclass که با برخی از تنظیمات تطابق داره، کلاینت می تونه به سادگی به دنبال Prototype اِ (نمونۀ اولیۀ) مناسب باشه و آن رو شبیه سازی کنه.
شیوۀ پیاده سازی دیزاین پترن Prototype
- اینترفیسِ Prototype رو بساز و تابع clone رو در آن اعلام کن.
- یک کلاس Prototype باید constructor جایگزینی رو تعریف کنه که شئ آن کلاس رو به عنوان آرگمان یپذیره. constructor باید value های همۀ فیلد های تعریف شده در کلاس رو از شئ فرستاده شده در نمونۀ جدید ساخته شده کپی کنه.
اگر در حال تغییر یک subclass هستی باید سازندۀ کلاس والد فراخوانی کنی تا به ابرکلاس اجازه بدی شبیه سازی فیلد های خصوصی خودش رو انجام بده.
- متد شبیه سازی معمولا از یک خط تشکیل می شه: ایجاد یک شئ جدید با آن constructor که ویژۀ Prototype هست.
توجه داشته باش که هرکلاس باید صراحتاً متد شبیه سازی را override کنه و با استفاده از نام کلاس خودش شئ بسازه. در غیر این صورت متد شبیه سازی ممکنه یک شئ از کلاس والد تولید کنه.
- به صورت اختیاری می تونی یک رجیستری Prototype متمرکز برای ذخیرۀ کاتالوگ Prototype های پرکاربرد ایجاد کنی.
می تونی ریجستری رو به عنوان یک کلاس Factory جدید پیاده سازی کنی یا آن رو یک متد static برای واکشی Prototype در کلاس Prototype (نمونۀ اولیه) پایه قرار بدی(کلاس Prototype پایه یا Base Prototype همان کلاس Shape در مثال قبل بود)
این Method باید یک Prototype رو بر بنیاد معیار های جست و جو، جست و جو کنه که کد مشتری به Method می فرسته. معیارها ممکنه یک برچسب رشتۀ ساده یا مجموعۀ پیچیده ای از پارامترهای جست و جو باشه.
پس از یافته شدن Prototype مناسب، رجیستری باید آن رو شبیه سازی کنه و کپی آن رو به client بر گردونه.
در آخر صدا زدن مستقیم سازنده های subclass ها رو با صدا زدنFactory Method اِ ریجستری Prototype جایگزین کن(دیزاین پترنFactory Method رو که پیشتر در سایت قرار دادیم که بخاطر داری؟)
مزایا و معایب دیزاین پترن Prototype
مزایای دیزاین پترن Prototype
- تو می تونی شئ ها رو بدون اتصال به کلاس های concrete ی آنها شبیه سازی کنی.
- تو می تونی از شر کد اولیه سازی مکرر به نفع شبیه سازی Prototype (نمونۀ اولیه: نمونه ای که از اول ایجاد شده) های از پیش ساخته شده خلاص بشی.
- تو می تونی شئ های پیچیده رو راحت تر تولید کنی
- هنگامی که با تنظیمات از پیش ساخته شده برای شئ ای پیچیده سروکار داری، جایگزینی برای وراثت دریافت می کنی
معایب دیزاین پترن Prototype
- شبیه سازی شئ های پیچیده که دارای reference های دایره ای هستند ممکنه بسیار مشکل باشه.
رابطۀ دیزاین پترن Prototype با دیگر دیزاین پترن ها
- بسیاری از دیزاین پترن ها با دیزاین پترن Factory Method آغاز می شن(پیچیده تر و قابل تنظیم تر از طریق subclass ها) و به سمت Abstract Factory، Prototype یا Builder (انعطاف پذیر تر، اما پیچیده تر) تکامل می یابن.
- کلاس های Abstract Factory غالبا بر پایۀ مجموعه ای از Factory Method هایند، اما تو همچنین می تونی از Prototype برای ترکیب Method های این کلاس ها استفاده کنی.
- زمانی که نیاز به ذخیرۀ کپی از دستورهای در تاریخچه داری، دیزاین پترن Prototype می تونه بهت یاری برسونه.
- دیزاین پترن (الگوهای طراحی) هایی که به شدت از Composite و Decorator استفاده می کنن اغلب می تونن از استفاده از الگوی طراحی Prototype سود ببرن.
- دیزاین پترن Prototype بر بنیاد وراثت نیست، بنابرین اشکال های ناشی از وراثت رو نداره. از سوی دیگه، دیزاین پترن Prototype به یک مقداردهی اولیۀ پیچیده از شئ شبیه سازی شده نیاز داره. Factory Method بر بنیاد وراثته اما نیازی به مرحلۀ اولیه سازی نداره.
- گاهی اوقات دیزاین پترن Prototype می تونه جایگزین ساده تری برای Memento باشه. اگر شئ، وضعیتی که می خواهی در تاریخچه ذخیره کنی، نسبتا ساده باشه و پیوندی با منبع های خارجی نداشته باشه، یا پیوند ها به راحتی دوباره برقرار بشن، کار می کنه.
- Abstract Factory ها، Builder ها و Prototype ها می تونن به عنوان singleton ها پیاده سازی بشن.
یک پروژۀ کوچک کاتلینی با استفاده از دیزاین پترن Prototype
دیزاین پترن Prototype یک دیزاین پترن Creational ای هست که امکان شبیه سازی شئ ها رو می ده، حتی اگر بسیار پیچیده باشن، بدون متصل شدن به کلاس های مشخص آنها.
همۀ کلاس های Prototype باید یک interface مشترک داشته باشن که امکان مشخص کردن شئ ها رو حتی اگر کلاس های concrete ی آنها ناشناخته باشه فراهم می کنه. شئ های نمونۀ اولیه می تونن کپی کامل تولید کنن زیرا شئ های یک کلاس یکسان می تونن به فیلد های خصوصی یکدیگر دسترسی داشته باشن.
دیزاین پترن Prototype تقریبا محبوب هست و زیاد پیچیده نیست.
دیزاین پترن Prorotype در کاتلین دردسترس هست. در کاتلین یک interface ای وجود داره با نام Cloneable
که با پیاده سازیش توسط هر کلاسی، امکان شبیه سازی شدن واسش فراهم می شه.
نحوۀ شناسایی : دیزاین پترن Prototype رو می شه به راحتی با تابع های clone و copy و… تشخبص داد.
در پروژه کوچکی با موضوع کپی کردن شکل های گرافیکی می خواهیم بدون استفاده از استاندارد اینترفیس Cloneable
دیزاین پترن Prototype رو پیاده سازی کنیم.
کلاس Shape
package prototype abstract class Shape { var x : Int = 0 var y : Int = 0 var color : String = "" constructor() constructor(target : Shape){ x = target.x y = target.y color = target.color } abstract fun clone() : Shape override fun equals(other: Any?): Boolean { if (other !is Shape) return false val shape2 : Shape = other return x == shape2.x && y == shape2.y && color == shape2.color } }
کلاس Circle
package prototype class Circle : Shape { var radius : Int = 0 constructor() : super() constructor(target : Circle) : super(target){ radius = target.radius } override fun clone(): Shape { return Circle(this) } override fun equals(other: Any?): Boolean { if (other !is Circle || !super.equals(other)) return false val shape2 : Circle = other return shape2.radius == radius } }
کلاس Rectangle
package prototype class Rectangle : Shape{ var width : Int = 0 var height : Int = 0 constructor() : super() constructor(target : Rectangle) : super(target){ width = target.width height = target.height } override fun clone(): Shape { return Rectangle(this) } override fun equals(other: Any?): Boolean { if (other !is Rectangle || !super.equals(other)) return false val shape2 : Rectangle = other return width == shape2.width && height == shape2.height } }
دمو :
package prototype class Demo { companion object{ fun basePrototype(){ val shapes = mutableListOf<Shape>() val shapesCopy = mutableListOf<Shape>() val circle = Circle() circle.x = 10 circle.y = 20 circle.radius = 15 circle.color = "red" shapes.add(circle) val anotherCircle = circle.clone() shapes.add(anotherCircle) val rectangle = Rectangle() rectangle.width = 10 rectangle.height = 20 rectangle.color = "blue" shapes.add(rectangle) cloneAndCompare(shapes,shapesCopy) } private fun cloneAndCompare(shapes : MutableList<Shape>, shapesCopy : MutableList<Shape>){ for(shape in shapes){ shapesCopy.add(shape.clone()) } for (i in 0 until shapes.size) { if (shapes[i] !== shapesCopy[i]) { //شکل ها، شئ های متفاوتی هستند print("شکل ها، شئ های متفاوتی هستند") println(" : $i") if (shapes[i].equals(shapesCopy[i])) { //و آنها همسان هستند print("و آنها همسان هستند") println(" : $i") } else { //اما آنها همسان نیستند print("اما آنها همسان نیستند") println(" : $i") } //همسان بودن یعنی شبیه سازی شده بودن } else { //شکل ها، از یک شئ هستند print("شکل ها، از یک شئ هستند") println(" : $i") } } } } }
تابع main :
fun main() { Demo.basePrototype() }
خروجی :
شکل ها، شئ های متفاوتی هستند : 0 و آنها همسان هستند : 0 شکل ها، شئ های متفاوتی هستند : 1 و آنها همسان هستند : 1 شکل ها، شئ های متفاوتی هستند : 2 و آنها همسان هستند : 2
Prototype Registry
تو می تونی یک Prototype رجیستری (یا Factory) متمرکز پیاده سازی کنی که شامل مجموعه ای از شئ های Prototype از پیش تعریف شده باشه. بدین ترتیب تو می تونی شئ های جدید رو از Factory با فرستادن نام یا پارامتر های دیگر بازیابی کنی.
Factory یک Prototype مناسب رو جست و جو می کنه، آن رو شبیه سازی می کنه و یک کپی رو بهت برمی گردونه.
کلاس BundledShapeCache (که برای cache کردن کاربرد داره)
package prototype.cache import prototype.Circle import prototype.Rectangle import prototype.Shape class BundledShapeCache { private val cache : MutableMap<String,Shape> = HashMap() init{ val circle = Circle() circle.apply { x = 5 y = 7 radius = 45 color = "سبز" } val rectangle = Rectangle() rectangle.apply { x = 6 y = 9 width = 8 height = 10 color = "آبی" } cache["دایرۀ سبز بزرگ"] = circle cache["مستطیل آبی متوسط"] = rectangle } fun put(key: String, shape: Shape): Shape { cache[key] = shape return shape } operator fun get(key: String): Shape? { return cache[key]?.clone() } }
دمو :
package prototype import prototype.cache.BundledShapeCache class Demo { companion object{ fun registryPrototype(){ val cache = BundledShapeCache() val shape1 = cache.get("دایرۀ سبز بزرگ") val shape2 = cache.get("مستطیل آبی متوسط") val shape3 = cache.get("مستطیل آبی متوسط") if (shape1 !== shape2 && shape1 != null && !shape1.equals(shape2)) { println("دایرۀ سبز بزرگ با مستطیل آبی متوسط نابرابر است") } else { println("دایرۀ سبز بزرگ با مستطیل آبی متوسط برابر است") } if (shape2 !== shape3) { println("مستطیل های آبی متوسط دو شئ متفاوت هستند") if (shape2 != null && shape2.equals(shape3)) { //همسان بودن یعنی شبیه سازی شده بودن println("و آنها همسان هستند") } else { println("اما همسان نیستند") } } else { println("شئ های مستطیل ها یکی هستند") } //از عملگر های //[] ، == //بجای //get ، equals //برای جلوگیری از پیچیدگی در کد استفاده نکردم //اگر دوست داشتید آنها رو جایگزین کنین //در کلاس هایی که تابع برابری بازنویسی شده //بهتر است تابع هش کد نیز بازنویسی شود //که در راستایی جلوگیری از پیچیدگی کد //از این کار خودداری کردم } } }
تابع main :
fun main() { Demo.registryPrototype() }
خروجی :
دایرۀ سبز بزرگ با مستطیل آبی متوسط نابرابر است مستطیل های آبی متوسط دو شئ متفاوت هستند و آنها همسان هستند
مقالۀ آموزش دیزاین پترن singleton در کاتلین در اینجا