یک فنجان جاوا - دیزاین پترن ها - Decorator

میتونین برای آشنایی با الگوهای طراحی (یا همون دیزاین پترن های) زبان جاوا به مقاله ی یک فنجان جاوا - دیزاین پترن ها Design Patterns مراجعه کنین.

(همونطور که گفته شده این الگو زیرمجموعه ی الگوهای ساختاری (Structural) هست)

الگوی Decorator

این الگو کمک میکنه که بدون تغییر در ساختار یه شئ، بتونیم تغییراتی اعمال و یا عملکردی بهش اضافه کنیم. دلیل اصلی اینکه این الگو زیرمجموعه الگوهای ساختاری هست هم همین مورده، به عبارتی ما داریم کلاس موجود رو پکیجی در نظر میگیریم که تغییرش ندیم و تغییرات مورد نیازمون رو به صورت دیگه ای به شئ ای که از اون کلاس درست شده، اعمال کنیم.
در حقیقت این الگو به ما کمک میکنه که بصورت داینامیک، عملکردها و رفتارهایی رو که میخوایم به یک شئ اعمال کنیم، بدون اینکه به ساختار کلاس و سایر اشیائ ساخته شده از همون کلاس کاری داشته باشیم.
قبل از اینکه به پیاده سازی مثال برسیم، به این نکته توجه کنیم که خب شاید بشه از ارثبری هم استفاده کرد! ولی به سه دلیل استفاده از این الگو اولویت بالاتری داری:
نکته ی اول، با دیدن مثالهای زیر متوجه میشیم که این الگو در نهایت دستمون رو خیلی بازتر میذاره و قابلیت توسعه پذیری بالاتری بهمون میده، و با اینکه شاید بعضی وقتا برای پیاده سازی، کمی پیچیدگی اضافه کنه، ولی در مجموع اگه سر جای خودش استفاده بشه بسیار ساخت یافته تر هست.
نکته ی دوم، زمانی که تعداد کلاس کمی ارثبری میکنن شاید بتونیم به یسری موارد دقت کنیم و سعی کنیم پیچیدگی رو کم کنیم، ولی با زیاد شدن کلاسها، این الگو به ما کمک میکنه که بتونیم مدیریت بهتری روی اونها داشته باشیم.
و نکته ی سوم هم اینکه ما با استفاده از ارثبری، در زمان کامپایل تمام ویژگی های کلاس پدر رو به فرزندانش و طبیعتاً به همه ی اشیائ اون کلاس ها منتقل میکنیم، در صورتی که با الگوی decorator، میتونیم در زمان اجرا یا همون runtime، تغییرات دلخواهمون رو بر اساس نیازمون، در یک شئ خاص استفاده کنیم.


خب بریم سراغ مثال. میخوایم بستنی بسازیم! در حقیقت ما یه کلاس مرجع برای بستنی مینویسیم، و اجازه میدیم هر بستنی بتونه با توجه به نوع خودش، تغییراتی که میخواد رو داشته باش. برای این منظور اول یه اینترفیس به اسم بستنی ایجاد میکنیم:

public interface Icecream {
    public String makeIcecream();
}

حالا نوبت میرسه به ساخت کلاس مرجع بستنی، کلاس BaseIcecream رو به صورت زیر پیاده سازی میکنیم:

public class BaseIcecream implements Icecream {
    @Override
    public String makeIcecream() {
        return "Base Icecream";
    }
}

این کلاس برای ساختن یه بستنی کفایت میکنه. شما اگه میخواین یه بستنی ساده بدون هیچ جنگولک بازی ای داشته باشین، فقط کافیه تابع makeIcecream از این کلاس رو فراخوانی بکنین تا بستنی ساده تون درست بشه! ولی هممون میدونیم که با یه بستنی ساده نمیشه بستنی فروشی باز کنین، البته مگرینکه یه شعبه اکبر مشتی بزنین فقط با بستنی سنتی. بگذریم، میخوایم با استفاده از این لگو، این امکان رو فراهم کنیم که بتونیم بستنی های ویژه با طعمای مختلف تهی کنیم. اول یه کلاس abstract که توی این الگو اصطلاحاً بهش decorator میگن، تولید میکنیم:

abstract class IcecreamDecorator implements Icecream {
    protected Icecream specialIcecream;
    public IcecreamDecorator(Icecream specialIcecream) {
        this.specialIcecream = specialIcecream;
    }
    public String makeIcecream() {
        return specialIcecream.makeIcecream();
    }
}

خب یکم این کلاس رو بررسی کنیم.
اولاً این کلاس abstract هست پس نمیتونیم هیچ شئ ای از این کلاس مستقیماً ایجاد کنیم، و در ادامه میبینیم که به ازای هر طعم، یه کلاس مخصوص به خودش رو تولید میکنیم که همشون از این کلاس ارث میبرن.
دوماً این کلاس اینترفیس Icecream رو implement کرده، و تابع makeIcecream رو داخل خود پیاده سازی کرده.
سوماً این کلاس توی تابع سازندش حتماً یه شئ ای میگیره که از نوع Icecream هست، و توی تابع makeIcecream داره makeIcecream اون شئ رو صدا میزنه.

خب حالا که کلاس مرجع decoratorمون رو پیاده کردیم، نوبت میرسه به پیاده سازی decorator های مربوط به هر طعم برای بستنیامون. بذارین با بستنی عسلی شروع کنیم. برای این منظور، کلاس HoneyDecorator رو بصورت زیر پیاده سازی میکنیم:

public class HoneyDecorator extends IcecreamDecorator {
    public HoneyDecorator(Icecream specialIcecream) {
        super(specialIcecream);
    }
    public String makeIcecream() {
        return super.makeIcecream() + addHoney();
    }
    private String addHoney() {
         return " + sweet honey";
    }
}

خب چیکار کردیم؟ این کلاس از IcecreamDecorator ارث میبره و ویژگی هایی که بالاتر برای IcecreamDecorator گفتیم رو داره، با این تفاوت که داخل تابع makeIcecream علاوه بر صدا زدن تابع makeIcecream کلاس IcecreamDecorator، داریم به کمک تابع addHoney طعم عسل رو بهش اضافه میکنیم. دقیقاً همین کار رو برای کلاس دیگه ای برای اضافه کردن آجیل به بستنیمون انجام میدیم:

public class NuttyDecorator extends IcecreamDecorator {
    public NuttyDecorator(Icecream specialIcecream) {
        super(specialIcecream);
    }
    public String makeIcecream() {
        return super.makeIcecream() + addNuts();
    }
    private String addNuts() {
        return " + cruncy nuts";
    }
}

خب تا اینجا اول یه اینترفیس به اسم Icecream ایجاد کردیم، کلاس مرجعی به اسم BaseIcecream پیاده سازی کردیم. بعدش IcecreamDecorator رو ایجاد کردیم که دو تا کلاس HoneyDecorator و NuttyDecorator از اون ارثبری کردن که بتونن آپشن عسل و آجیل رو هم به بستنیامون اضافه کنن.
حالا ببینیم چجوری میتونیم بستنی ویژه ی طعم دار تولید کنیم:

Icecream icecream = new HoneyDecorator(new NuttyDecorator(new BaseIcecream()));
System.out.println(icecream.makeIcecream());

ما یه BaseIcecream یا همون بستنی ساده تولید کردیم، اون رو دادیم به NuttyDecorator که آجیل بریزه روش، و بعدش دادیم به HoneyDecorator که عسل رو هم چاشنی کار کنه. و خروجی کد بصورت زیر خواهد بود:

Base Icecream + cruncy nuts + sweet honey

دقت کنیم که نهایتاً تابع makeIcecream هست که تعیین کننده ی نوع بستنی ماس، و وقتی کد میرسه به icecream.makeIcecream() در حقیقت اول تابع makeIcecream مربوط به کلاس HoneyDecorator صدا زده میشه، این تابع قبل از اینکه طعم عسل رو اضافه کنه، تابع makeIcecream مربوط به کلاس NuttyDecorator رو صدا میزنه برای آجیل دار کردن بستنی، و اون هم قبل از آجیل دار کردن بستنی، تابع makeIcecream مربوط به کلاس BaseIcecream رو صدا میزنه که بستنی ساده رو درست کنه. پس روند به این صورت میشه که اول بستنی ساخته میشه، بعد آجیل و نهایتاً عسل هم بهش اضافه میشه.

ما به همین راحتی میتونیم اول بستنی رو عسلی کنیم بعدش بدیم به آجیل:

Icecream icecream = new NuttyDecorator(new HoneyDecorator(new BaseIcecream()));

یا اینکه فقط بستنی ساده داشته باشیم:

Icecream icecream = new BaseIcecream();

یا اینکه فقط بستنی عسلی:

Icecream icecream = new HoneyDecorator(new BaseIcecream());

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

new HoneyDecorator(new NuttyDecorator(new HoneyDecorator(new NuttyDecorator(new HoneyDecorator(new BaseIcecream())))));

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


خب بریم سراغ یه مثال دیگه. یه اینترفیس به اسم Shape پیاده سازی میکنیم:

public interface Shape {
    void draw();
}

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

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Shape: Circle");
    }
}
public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Shape: Rectangle");
    }
}

خب حالا وقت چی رسیده؟ درسته، تولید decorator.

public abstract class ShapeDecorator implements Shape {
    protected Shape decoratedShape;
    public ShapeDecorator(Shape decoratedShape){
        this.decoratedShape = decoratedShape;
    }
    public void draw(){
        decoratedShape.draw();
    }	
}

الآن میخوایم دور شکها، بوردر یا همون خط خارجی بندازیم. کلاس زیر رو پیاده میکنیم:

public class RedShapeDecorator extends ShapeDecorator {
    public RedShapeDecorator(Shape decoratedShape) {
        super(decoratedShape);		
    }
    @Override
    public void draw() {
        decoratedShape.draw();	       
        setRedBorder(decoratedShape);
    }
    private void setRedBorder(Shape decoratedShape){
        System.out.println("Border Color: Red");
    }
}

این کلاس یه Shape میگیره و زمانی که میخواد اون رو رسم کنه، اول تابع draw اون شکل رو صدا میزنه، بعد خودش به کمک تابع setRedBorder دور شکل یه خط قرمز میندازه. برای اینکه این کد رو تست کنیم کافیه بصورت زیر عمل کنیم:

Shape circle = new Circle();
circle.draw();

تا اینجا که کار خاصی نکردیم، یه دایره ساختیم و خواستیم اون رو رسم کنیم، خروجی بصورت زیره:

Shape: Circle

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

Shape redCircle = new RedShapeDecorator(new Circle());
redCircle.draw();

و خروجی:

Shape: Circle
Border Color: Red

و برای ساختن به مستطیل با خط قرمز:

Shape redRectangle = new RedShapeDecorator(new Rectangle());
redRectangle.draw();

خروجی:

Shape: Rectangle
Border Color: Red

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

برای اینکه یکم بیشتر متوجه بشیم چیکار کردیم به دیاگرام uml زیر نگاهی بندازین:

طبیعتاً این فقط یه مثاله و میشه خیلی کارای دیگه هم کرد. مثلاً میتونم عملیات رنگ کردن رو هم بدیم به یه کلاس دیگه، یعنی یه کلاس داشته باشیم به اسم ShapeDecorator و یه کلاس دیگه داشته باشیم به اسم ColorDecorator که میتونه مشخص بکنه که ShapeDecorator چه رنگی داشته باشه، به این صورت که ShapeDecorator از ColorDecorator ارث ببره و ColorDecorator کلاس Shape رو implement بکنه. یا اینکه بتونیم رنگ داخل شکلها یا FillColor رو هم انتخاب کنیم، که برای این منظور هم میشه با اضافه کردن تابع setFillBorder، تابع draw رو بصورت زیر تغییر داد:

@Override
public void draw() {
    decoratedShape.draw();
    setFillBorder(decoratedShape);
    setRedBorder(decoratedShape);
}

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


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

public interface Shape {
    void draw();
    String description();
    boolean isHide();
}

و دو کلاس دایره و مستطیل را بصورت زیر پیاده سازی میکنیم:

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing Circle");
    }
    @Override
    public String description() {
        return "Circle object";
    }
    @Override
    public boolean isHide() {
        return false;
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing Rectangle");
    }
    @Override
    public String description() {
        return "Rectangle object";
    }
    @Override
    public boolean isHide() {
        return false;
    }
}

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

public abstract class ShapeDecorator implements Shape {
    protected Shape decoratedShape;
    public ShapeDecorator(Shape decoratedShape) {
        super();
        this.decoratedShape = decoratedShape;
    }
}

خب میخوایم ویژگی های جدید و جذابمون رو اضافه کنیم، اول از همه میخوایم تنوع رنگ داشته باشیم پس enum زیر رو مینویسیم:

public enum Color {
    RED,
    GREEN,
    BLUE,
    YELLOW,
    WHITE,
    BLACK,
    ORANGE,
    MAROON
}

و برای اینکه پروژه جذابتر بشه، میخوایم نوع خط رو هم در اختیار کاربر بذاریم که معمولی باشه یا خط چین یا نقطه نقطه یا ...

public enum LineStyle {
    SOLID,
    DASH,
    DOT,
    DOUBLE_DASH,
    DASH_SPACE
}

خب داره شیرین تر میشه ماجرا. الآن میخوایم به هر شکلی که داریم، امکان FillColor یا رنگ داخل شکل رو اضافه کنیم، پس براش یه decorator پیاده میکنیم:

public class FillColorDecorator extends ShapeDecorator {
    protected Color color;
    public FillColorDecorator(Shape decoratedShape, Color color) {
        super(decoratedShape);
        this.color = color;
    }
    @Override
    public void draw() {
        decoratedShape.draw();
        System.out.println("Fill Color: " + color);
    }
    @Override
    public String description() {
        return decoratedShape.description() + " filled with " + color + " color.";
    }
    @Override
    public boolean isHide() {
        return decoratedShape.isHide();
    }
}

این کلاس، تابع isHide رو تغییر نمیده و تابع isHide مربوط به خود decoratedShape رو صدا میزنه. ولی در تابع description و draw از رنگ استفاده هایی میکنیم.

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

public class LineColorDecorator extends ShapeDecorator {
    protected Color color;
    public LineColorDecorator(Shape decoratedShape, Color color) {
        super(decoratedShape);
        this.color = color;
    }
    @Override
    public void draw() {
        decoratedShape.draw();
        System.out.println("Line Color: " + color);
    }
    @Override
    public String description() {
        return decoratedShape.description() + " drawn with " + color + " color.";
    }
    @Override
    public boolean isHide() {
        return decoratedShape.isHide();
    }
}

و حتی برای جذابتر شدن، قابلیت تعیین قطر خط رو هم براش کشیدن شکلها اضافه میکنیم:

public class LineThinknessDecorator extends ShapeDecorator {
    protected double thickness;
    public LineThinknessDecorator(Shape decoratedShape, double thickness) {
        super(decoratedShape);
        this.thickness = thickness;
    }
    @Override
    public void draw() {   
        decoratedShape.draw();
        System.out.println("Line thickness: " + thickness);
    }
    @Override
    public String description() {
        return decoratedShape.description() + " drawn with line thickness " + thickness + ".";
    }
    @Override
    public boolean isHide() {
        return decoratedShape.isHide();
    }
}

و در نهایت نوبت میرسه به تعیین نوع خط:

public class LineStyleDecorator extends ShapeDecorator {
    protected LineStyle style;
    public LineStyleDecorator(Shape decoratedShape, LineStyle style) {
        super(decoratedShape);
        this.style = style;
    }
    @Override
    public void draw() {
        decoratedShape.draw();
        System.out.println("Line Style: " + style);
    }
    @Override
    public String description() {
        return decoratedShape.description() + " drawn with " + style + " lines.";
    }
    @Override
    public boolean isHide() {
        return decoratedShape.isHide();
    }
}

و تمام. برای مثال، یه دایره و یه مستطیل تعریف میکنیم:

Shape rectangle = new Rectangle();
Shape circle = new Circle();

بصورت عادی میتونن رسم بشن:

rectangle.draw();
circle.draw();

خروجی:

Drawing Rectangle
Drawing Circle

یه دایره ی جدیدی با ویژگی های دلخواهمون تعریف میکنیم:

Shape circle1 = new FillColorDecorator(new LineColorDecorator(new LineStyleDecorator(new LineThinknessDecorator(new Circle(), 2.0d), LineStyle.DASH), Color.BLUE), Color.RED);

circle1.draw();

خروجی:

Drawing Circle
Line thickness: 2.0
Line Style: DASH
Line Color: BLUE
Fill Color: RED

همین شئ رو میشه بصورت زیر هم تعریف کرد که همون خروجی قبلی رو بهمون میده:

Circle c = new Circle();
LineThinknessDecorator lt = new LineThinknessDecorator(c, 2.0d);
LineStyleDecorator ls = new LineStyleDecorator(lt, LineStyle.DASH);
LineColorDecorator lc = new LineColorDecorator(ls, Color.BLUE);
FillColorDecorator fc = new FillColorDecorator(lc, Color.RED);
Shape circle2 = fc;

circle2.draw();

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

Shape circle3 = new FillColorDecorator(new LineColorDecorator(new Circle(), Color.BLACK), Color.GREEN);

circle3.draw();

خروجی:

Drawing Circle
Line Color: BLACK
Fill Color: GREEN

و یا مستطیلی با رنگ داخلی زرد و خط قرمز:

Shape rectangle1 = new FillColorDecorator(new LineColorDecorator(new Rectangle(), Color.RED), Color.YELLOW);

rectangle1.draw();

خروجی:

Drawing Rectangle
Line Color: RED
Fill Color: YELLOW

همونطور که دیدیم، با استفاده از الگوی decorator بدون اینکه کلاسای اصلیمون رو تغییری بدیم (Shape, Circle, Rectangle)، تونستیم ویژگی هایی که میخوایم رو بهشون اضافه کنیم، مثل نوع خط، قطر خط و رنگ ها. در حقیقت کاری به ماهیت دایره و مستطیل نداشتیم و فقط انتخاب ویژگی هاشون رو بدون دستکاری کلاس های شکلهامون به کدمون اضافه کردیم. البته درسته که وقتی شکل میخواد کسیده بشه باید ویژگی هایی که گفتیم رو بدونه بعد کشیده بشه ولی خب این یه مثاله برای اینکه روند و ترتیب اجرا، ساختار، و قسمتی از قابلیت توسعه پذیری این الگو بهتر درک بشه.


منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian

https://virgool.io/@mohammad.ghodsian/java-decorator-design-pattern-y7nat0ejmpja