Constructorهای عالی اینچنین باید باشند
Constructorها عالی هستند. اما برای داشتن کد تمیز و قابل فهمتر باید نکاتی را در استفاده از Constructorها در نظر بگیریم.
در کد، هدف باید تنظیم کردن همه چیز در constructor باشد و تا جای ممکن باید از setterها در کد اجتناب کرد چرا که ثبات و تغییرناپذیری در برنامه تا جای ممکن باید حفظ شود و setterها دشمن این مساله هستند.
به علاوه، این تنها کاری است که constructor باید انجام دهد. هیچ منطق پیچیدهای را نباید در Constructor اشیا قرار داد. در واقع باید جایی تنها برای فیلدهای اشیا باشد. هرچیز دیگر مثل اتصال به سرویسهای دیگر باید در متد init قرار گیرد. ترجیحا این متد باید فاقد پارامتر بوده و هرکاری که لازم است تا شی را به وضعیت صحیح ببرد در این متد انجام میگیرد.
public class DoesSomeComplexConnection { private final String dbUrl; private final String user; public DoesSomeComplexConnection(String dbUrl, String user){ this.dbUrl = dbUrl; this.user = user; } public void init(){ connectToDb(dbUrl, user); }
به عنوان یک قانون، همواره سعی کنید از یک constructor استفاده کنید. این کار فهم کد را راحتتر میکند. اشیا شما باید مجموعهای از قابلیتها داشته باشند که همواره براساس مجموعهای از وابستگیها اجرا میشود. اگر شما، به عنوان فراخواننده، نیازی به این قابلیتها نداشتید، آنگاه از قابلیت بدون عملیات استفاده میکنید.
public DoesSomeComplexConnection(String dbUrl, String user, Notifier notifyOnConnection){ this.dbUrl = dbUrl; this.user = user; this.notifyOnConnection = notifyOnConnection; } public void init(){ connectToDb(dbUrl, user); notifyOnConnection.connected(); } public static void main(String[] args) { new DoesSomeComplexConnection(dbUrl, user, new DoNothingOnNotification()) } private static class DoNothingOnNotification implements Notifier{ @Override public void connected() { } } private interface Notifier { void connected(); }
که این مساله در جاوا ۸ نیز واضحتر خواهد بود.
new DoesSomeComplexConnection(dbUrl, user, () -> {});
یکی از مهمترین بخشهای کد این است که شما نباید هرگز null را پاس بدهید. بهکارگیری چنین قانون سادهای در کد به این معناست که شما میتوانید تعداد زیادی خطوط کد را که برای چک کردن null بودن یا نبودن است، حذف کنید.
اگر کد شما قابل تغییر نیست، آنگاه چک کردن nullها را در constructor انجام دهید. به عبارتی در انتهای constructor مطمئن باشید که تمام فیلدها تنظیم شده و شئ شما آماده است.
گاهی پاسخ این مساله، constructorهای چندگانه در نظر گرفته میشود، که پیادهسازی پیشفرض را فراهم میکنند. constructorهای چندگانه کد را پیچیده میکنند و فهم آن را دشوار میسازند. اما داشتن پیادهسازی پیش فرضی برای end userها خوب است. مثل زیر:
public class DoesSomeComplexConnection { public static final Notifier NO_OP_NOTIFIER = () -> {}; … } new DoesSomeComplexConnection(dbUrl, user, DoesSomeComplexConnection.NO_OP_NOTIFIER);
یک مثال عملی:
بیایید نگاهی به کد زیر بیندازیم:
public class Reader { public Reader(Swagger swagger) { this(swagger, null); }
public Reader(Swagger swagger, ReaderConfig config) { this.swagger = swagger == null ? new Swagger() : swagger; this.config = new DefaultReaderConfig(config); } } public class DefaultReaderConfig implements ReaderConfig { /** * Creates default configuration. */ public DefaultReaderConfig() { } /** * Creates a copy of passed configuration. */ public DefaultReaderConfig(ReaderConfig src) { if (src == null) { return; } setScanAllResources(src.isScanAllResources()); setIgnoredRoutes(src.getIgnoredRoutes()); }
این کد به چند دلیل فاجعه است!
- nullها چپ راست و وسط رها شدهاند. این کار فهم اتفاقی که دارد میافتد را سخت کرده و منجر به کد اضافی برای چک کردن nullها شده است.
- وجود constructor چندگانه به خصوص وقتی که اولی فقط null دیگری را پاس میدهد.
- ساخت شیئ دیگر در constructor، این خیلی بد نیست و احتمالا شما هم چنین حسی نسبت به آن دارید. اما چون که تنها یک پارامتر را میپذیرد نوعی decorator به حساب میآید. چرا شئ مورد نیاز را پاس ندهیم؟
- به نظر میرسد که شئ مورد نیازی را پاس میدهد اما تنها آن را دوباره wrap میکند تا از آن مطمئن باشد.
اول میتوان اولین constructor را حذف کرد. هرکس که آن را بخواهد، null پاس خواهد داد.
دوم اگر شما میخواهید مردم را بپذیرید، نباید null پاس داد. پس میتوان کد چک کردن null بودن Swagger را حذف کرد. اگر این مشکلزا شود، برنامه به سرعت منهدم خواهد شد و ما میتوانیم کدهای وابسته آن را اصلاح کنیم. پس خواهیم داشت:
public class Reader { public Reader(Swagger swagger, ReaderConfig config) { this.swagger = swagger; this.config = new DefaultReaderConfig(config); } }
خب الان بهتر شد. حالا دیگر نیازی به wrap برای چک کردن null نیست. DefaultReaderConfig تنها پیادهسازی در دسترس خواهد بود. اما اجازه دهید فرض کنیم این گونه نیست آنگاه کد را تغییر میدهیم تا شی را مستقیما پاس بدهیم و نه اینکه داخل constructor بسازیم.
public class Reader { public Reader(Swagger swagger, DefaultReaderConfig config) { this.swagger = swagger; this.config = config; } }
حالا کد خیلی تمیزتر به نظر میرسد. اما اگر کسی configای برای پاس دادن نداشت چه میشود؟ پیادهسازی پیش فرض!
public class Reader { public static final DefaultReaderConfig DEFAULT_CONFIG = new DefaultReaderConfig() public Reader(Swagger swagger, DefaultReaderConfig config) { this.swagger = swagger; this.config = config; } } //Someone using this class new Reader(swagger, Reader.DEFAULT_CONFIG)
در نتیجه ما از شر دو constructor در ReaderConfig راحت خواهیم شد و کد تمیزتر و سادهتری برای فهم خواهیم داشت.
منبع: