مجموعه‌ی Effective Java : آیتم 2
۳۰ مهر ۹۷ ، ۲۰:۳۳
سجاد یوسف نیا ۰ نظر Share

مجموعه‌ی Effective Java : آیتم 2

اگه توجه کنید کانسترکتورها و static factory methodها یک محدودیت و مشکل مشترکی رو دارن، اون اینه که : وقتی اونا تعداد پارامترهای اختیاری‌شون زیاد میشه کارمون دچار مشکل میشه.
فرض کنید یک کلاسی داریم که یک مربوط اطلاعات تغذیه‌ای مربوط به غذاهای مختلفی رو به ما میده. و این کلاس هم شامل فیلدهای اطلاعاتی‌ای مثل کالری، کربوهیدرات، چربی و... باشه. میشه گفت که هیج غذایی رو نمیتونیم پیدا کنیم که توی اون این فیلدها برابر با صفر بشه.
حالا باید چه نوع static factory method یا کانسترکتوری برای این کلاس استفاده کنیم؟
برنامه‌نویس‌ها به طور معمول از الگوی کانسترکتور تلسکوپی استفاده میکنن، یعنی به این صورت که چندین کانسترکتور داشته باشیم و هر کدوم به تعدادی که ممکنه احتیاج داشته باشیم، پارامتر بگیره، مثلا یک کانسترکتور یکی بگیره، یکی دیگه 2 تا و دیگری 3 تا و... .
پس با این حساب چندین کانسترکتور خواهیم داشت، یک مثالی رو میتونید پایین ببینید :
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; // (mL)
required
private final int servings;
// (per container) required
private final int calories;
// (per serving)
optional
private final int fat;
// (g/serving)
optional
private final int sodium;
// (mg/serving)
optional
private final int carbohydrate; // (g/serving)
optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}ITEM 2: CONSIDER A BUILDER WHEN FACED WITH MANY CONSTRUCTOR PARAMETERS
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings
= servings;
this.calories
= calories;
this.fat
= fat;
this.sodium
= sodium;
this.carbohydrate = carbohydrate;
}
}

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

NutritionFacts cocaCola =
new NutritionFacts(240, 8, 100, 0, 35, 27);
خیلی مواقع توی این روش باید پارامترهایی رو بفرستیم که مورد استفاده‌ی ما نیستن، ولی به هر جهت مجبوریم بفرستیم، چون در غیر این صورت با ارور مواجه میشیم. مثلا توی تابع بالا ما به جای fat مقدار 0 رو فرستادیم. اینجا که 6 پارامتر داریم ممکنه کارمون زیاد سخت نشه ولی با افزایش تعداد پارامترها کار خیلی گره میخوره.
به طور خلاصه اینکه، الگوی تلسکوپی رو میشه درست و حسابی عملی کرد ولی انجام این کار باعث میشه جایی که میخوایم از کانسترکتور استفاده کنیم نوشتن کد، به دلیل زیاد بودن تعداد پارامترها سخت بشه و توی خوندنش هم مشکل پیدا کنیم. انجام این کار باعث گیج شدن کسی که میخواد از کلاس استفاده کنه میشه و وجود پارامترهای دارای نوع یکسان میتونه باعث باگهای کوچکی هم بشه، چون در صورتی که کاربر اشتباها جای این دو تا پارامتر هم‌نوع رو عوض کنه کامپایلر به ما تذکری نخواهد داد. ولی این باگ ممکنه که خودش رو موقع کامپایل نشون بده ( که توی آیتم 51 در مورد این توضیح داده شده ).
یک جایگزینی که میتونیم برای حل مشکل زیاد بودن تعداد پارامترهای کانسترکتور استفاده کنید، الگوی JavaBeans هست. که توی اون، شما یک کاسنترکتور بدون پارامتر رو فراخوانی میکنید و بعدش متدهای setter رو برای هر پارامتری که لازم دارید، استفاده میکنید :
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings
= -1; // Required; no default value
private int calories
= 0;
private int fat
= 0;
private int sodium
= 0;
private int carbohydrate = 0;
public NutritionFacts() { }
  // Setters
  public void setServingSize(int val){
    servingSize = val; 
  }
  public void setServings(int val) {
    servings = val;
  }
  public void setCalories(int val) {
    calories = val;
  }
  public void setFat(int val){
    fat = val; 
  }
  public void setSodium{
    sodium = val;
  }
  public void setCarbohydrate(int val){
    carbohydrate = val;
  }

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

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
ولی متاسفانه، این الگوی JavaBeans خودش مشکلات مهمی داره. به این خاطر که ساختار ما جوریه که چندین فراخوانی یا call داخلش داریم، ممکنه یه چیزی از آب دربیاد که چند تا از stateهاش به اصطلاح inconssistent باشن، ( بذارید مفهوم inconssitency رو با یک مثال براتون روشن کنم. فرض کنید که ما یک کلاسی داریم که از یک کلاسی ارث‌بری داره، حالا فرض کنید اگه توی کلاس فرزند همه‌ی اینترفیس‌های پدر رو implement نکنیم inconssistency رخ میده. حالا فرض کنید که اگه setterای رو برای یکی از propertyها یادمون بره مشابه همچین وضعیتی رخ داده ).
این کلاس هم که قدرت اینو نداره که با اعتبارسنجی پارامترهای کانسترکتور، inconssistency به وجود اومده رو از بین ببره.
از طرفی هم کلاس نمیتونه و راهی هم نداره که بشه با اون وجود conssistency در پارامترهای کانسترکتور رو تضمین کرد. استفاده کردن از آبجکت، موقعی که inconssistent هست موجب خطاهایی میشه که دیباگ کردنشون به دلایلی سخته. یک مشکل دیگه‌ای که این الگو داره اینه که باعث میشه نتونیم یک کلاس immutableای رو بسازیم ( آیتم 17 ) و هم زحمت برنامه نویس رو برای thread safe نگه داشتن کد، بیشتر میکنه.
این مشکلاتو میشه با استفاده از < فریز کردن > آبجکت، موقعی که داره ساخته میشه و همینطور جلوگیری از استفاده از آبجکت تا زمانی که فریز نشده، کمتر کرد. ولی این روش خیلی کم و به ندرت به کار میاد. علاوه بر این میتونه باعث ایجاد runtime errorهایی هم بشه، چون کامپایلر نمیتونه تضمین کنه که برنامه‌نویس قبل از استفاده از آبجکت اون رو فریز میکنه.
خوشبختانه راه سومی هم وجود داره که امنیت الگوی تلسکوپی و خوانایی الگوی JavaBeans رو با خودش همراه داره. این حالت یک شکلی از الگوی Builder هست. به جای اینکه آبجکت مورد نظرمون رو مستقیما بسازیم. کلاینت یا مصرف‌کننده‌ی کلاس، یک کانسترکتور ( یا static facory ) رو با همه‌ی پارامترهای مورد نیازش فراخوانی میکنه و یک آبجکت از builder رو تحویل میگیره. بعدش کلاینت یا مصرف‌کننده‌ی کلاس، متدهایی مشابه با setter رو فراخوانی میکنه و هر پارامتری رو که میخواد، به صورت کاملا اختیاری مقداردهی میکنه. آخرش هم، یک متد build که بدون پارامتر هست رو برای تولید کردن آبجکت، فراخوانی میکنه، که این آبجکته اکثرا immutable هست. ‌معمولا Builder یک static member هست. طرز کارش رو پایین میتونید ببینید :
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;

public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;

// Optional parameters - initialized to default 
private int calories= 0;
private int fat= 0;
private int sodium= 0;
private int carbohydrate = 0;

public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings= servings;}

public Builder calories(int val){calories = val;
return this;
public Builder fat(int val)
{ fat = val;
return this;
public Builder sodium(int val)
{ sodium = val;
return this;
public Builder carbohydrate(int val)
{ carbohydrate = val; return this;
public NutritionFacts build() {
return new NutritionFacts(this);
}
}

private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings= builder.servings;
calories= builder.calories;
fat= builder.fat;
sodium= builder.sodium;
carbohydrate = builder.carbohydrate;
}
}

کلاس NutritionFacts  کلاس immuteableای هست و همه‌ی پارامترها در یک جای خاصی قرار داده شدن. متدهای setter ، خودشون آبجکت builder رو return میکنن، بنابراین به صورت زنجیروار میتونیم این متدها رو فراخوانی کنیم، و یک فایده‌ای که این کار برای ما داره اینه که بتونیم یک API روان و انعطاف‌پدیر داشته باشیم. حالا یک نگاهی به طرز کار کده میندازیم :

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();

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

خب لازمه یک توضیحی در مورد invariantها بدم و بعدش بحثمون رو ادامه بدیم، یک invariant در کلاس، یک propery هست که برای نگه داشتن instance یا instanceهایی در کلاس مورد استفاده قرار میگیره. برای توضیح بیشتر به اینجا مراجعه کنید. 

invariantهایی که چندین پارامتر  داخل خودشون دارن رو توی کانسترکتور درست موقعی که متد build فراخوانی میشه بررسی کنید. برای ایمن نگه داشتن invariantها مقابل حمله، بررسی هایی که میخواید رو object fieldها انجام بدید رو بعد از کپی کردن از builder انجام بدید. در صورتی که نتیجه‌ی بررسی شما موفقیت آمیز نبود یک IllegalArgumentException رو throw کنید که توی اون نشون بده که چه پارامترهای نامعتبرن. 

ابن الگوی Builder در وراثت خیلی خوب میتونه عمل کنه. میتونیم یکسری سلسله‌مراتب موازی رو داشته باشیم که از این کلاس شروع بشه. کلاس‌های abstract، باید Builder شون abstract باشه و کلاس‌های غیر abstract باید Builderشون غیر abstract باشه. مثلا فرض کنید که یک کلاس abstract در راس سلسله‌مراتب قرار داشته باشه با اسم کلاس Pizza که مثلا نشون‌دهنده‌ی انواع مختلف پیتزا باشه :

// Builder pattern for class hierarchies
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// Subclasses must override this method to return "this"
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // See Item 50
}
}

دقت کنید که Pizza.Builder یک نوع جنریک از نوع بازگشتی هست. این مساله بعلاوه‌ی متد abstractای به اسم self باعث میشن تا بتونیم به صورت زنجیری توابع رو پشت سر هم بذاریم و نیازی هم به castکردن نداشته باشیم. این جرکتی که زدیم برای جبران و شبیه‌سازی یک چیزی که در جاوا نیست، یعنی self-type هست.

اینجا دو تا زیرکلاس از Pizza رو داریم. اولی نیاز به پارامتر سایز داره ولی توی دومی میشه انتخاب کرد که پیتزا میخواد باید سس داشته باشه یا نه و اینکه میخواد داخل سرو بشه یا بیرون :) .

  

public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override public NyPizza build() {
return new NyPizza(this);
}
@Override protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // Default
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}

دقت کنید که توی هر زیرکلاس، builder برای return کردن زیرکلاس هست. برای مثال متد build توی NyPizza.‌Builder ه NyPizza رو return می‌کنه. در حالی که توی Calzone.Builder هم ،  Calzone ه ، return میشه. توی این روش، یک متدی از زیرکلاس برای return کردن چیزی که نوعش توی کلاس پدر ( super ) معلوم شده، استفاده میکنن. به این نوع از return کردن  covariant return هم میگن. این باعث میشه تا کسی که از کلاس ما استفاده میکنه، بتونه بدون نیاز به cast کردن کارشو انجام بده.

طرز استفاده ی کلاینتی از این نوع ( hierarchical builders ) با نحوه‌ی استفاده از builder مربوط به NutritionFacts یکسان هست. برای مثال، کد بعدی رو که میبینید، از ثابتهای enumaration استفاده کرده داخل متد :

NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();

اگه دفت کرده باشید یک مزیت کوچک دیگه‌ای هم که استفاده از builderها نسبت به استفاده از کانسترکتور داره اینه که، ما میتونیم - به خاطر اینکه به هر متد میشه فقط یک vararg و نه بیشتر فرستاد - چندین vararg رو بفرستیم، چون که هر کدومش داره توی یک متد مورد استفاده قرار میگیره.

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

الگوی Builder کاملا انعطاف‌پذیر هست. میتونیم چندین بار از این الگو برای ساخت آبجکت استفاده کنیم. میشه پارامترهای قبل متد build رو جاهاشون رو با هم عوض کرد و یک آبجکت جدیدی رو درست کرد. یک builder میتونه بعضی از پارامترها رو خودش به طور خودکار براساس ساخته شدن آبجکت، خودش بسازه. برای اینکه بهتر متوجه بشید، فرض کنید که یک شماره سریال داریم که هر وقت آبجکت ساخته میشه، اون شماره سریاله یکی افزایش پیدا میکنه.

الگوی Builder مشکلات خودش رو هم داره، مثلا برای اینکه بخوایم یک آبجکت رو ایجاد کنیم، اول بایستی builderاش رو بسازید. درسته که ما برای ساخت builder بهای زیادی رو پرداخت نمی‌کنیم و به قولی هزینه‌ی زیادی به ما تحمیل نمیشه، ولی استفاده از این روش، وقتی که عملکرد برای ما خیلی مهمه، زیاد جالب نیست. در ضمن حجم و مقدار کدی که برای الگوی Builder استفاده می‌کنیم، بیشتر از الگوی کانسترکتور تلسکوپی هست، برای همین استفاده از این الگوی Builder موقعی برامون صرفه داره که حداقل 4 تا پارامتر داشته باشیم. ولی دقت کنید که ممکنه شمل در آینده پارامترهای بیشتری رو به کلاسه اضافه کنید. اگه احتمال میدید که در آینده ممکنه تعداد پارامترها افزایش پیدا که بهتره که از Builder استفاده کنید، چون اگه اول کانسترکتور معمولی یا static factory method استفاده کنید، و بعدا بخواید به Builder سوییچ کنید به مشکل برخواهید خورد.

به طور خلاصه اینکه، وقتی دارین کلاسی رو طراحی میکنید که static factory method ها یا کانسترکتورهاش زیاد هستن،- به خصوص وقتی که پارامترها اختیاری هستن یا نوع یاکسانی دارن.- بهتره از الگوی Builder استفاده بشه. و اینکه در builderها خوندن و نوشتن آسون‌تر از الگوی تلسکوپی هست و از الگوی JavaBeans هم مطمین‌تر و ایمن تره.

اگه نظری یا انتقادی داشتید لطفا بفرمایید.

نظرات (۰)

هیچ نظری هنوز ثبت نشده است

ارسال نظر

ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی

تصاوير منتخب