كونك مبرمج وتعرف أساسيات البرمجة الكائنية OOP فقد يبدوا العنوان غريب حقاً، فكيف اقوم بانشاء كائن بدون ان أكتب أو استخدم دالة البناء Constructor ؟ وهنا سوف نوضح كيف يمكن الاستعاضة عن ال Constructor بال Static Factory وما هي الفروقات بينهم
هذه المقالة جزء من سلسلة كيف تحترف لغة الجافا وهي موجهة لأي مطور OOP ويستخدم لغة الجافا
- الإسلوب الصحيح لكتابه Utility Classes
- هل مللت من دالة البناء Constructor ؟ (المقال الحالي)
- استخدام ال Builder Pattern في دالة البناء
- لا تستخدم Public الا وقت الحاجة
- كيف تصمم الPackages جيداً في تطبيقات جافا
- جافا دائماً Passing by Value
- نظرة حول الدالة Equals
- خارطة طريق لتعلم الجافا
دالة البناء Public Constructor
لنبدأ بالطريقة العادية (او التقليدية الآن) لإنشاء كائن من كلاس معين وهو من خلال استدعاء دالة بناء public constructor داخل هذا الكلاس، مثلاً في السطر التالي قمنا باستدعاء دالة البناء في الطالب وقمنا بانشاء كائن منه:
public class Item1 { public static void main(String[] args) { Student student = new Student("Ahmed"); System.out.println("student name: " + student.getName()); } } class Student { public Student (String name) { this.name = name; } public String getName() { return this.name; } private String name; }
الى هنا هذا الأمر هو ما قمنا به خلال سنوات ، ولكن لما لا نلقى نظرة على طريقة أخرى يمكن أن تضيفها بجانب معلوماتك ؟ هذه الطريقة أخرى تمكننا من انشاء كائن من الطالب (كما هو الحال مع دالة البناء) وذلك بعمل دالة static ترجع نسخه من الطالب (أي داخل هذه الدالة ال static سيتم استدعاء دالة البناء -هي موجودة موجودة ولكن قمنا باضافة طبقة اخرى فوقها-) ، بالتالي بعد ادخال هذه الدالة للكلاس يصبح لدينا الكود بالشكل:
public class Item1 { public static void main(String[] args) { Student student1 = new Student("Ahmed"); Student student2 = Student.createStudent("Ali"); System.out.println("student name: " + student1.getName()); System.out.println("student name: " + student2.getName()); } } class Student { public Student (String name) { this.name = name; } public static Student createStudent (String name) { return new Student(name); } public String getName() { return this.name; } private String name; }
Static Factory
لاحظ الأن ان لدينا دالة static تقوم باستدعاء دالة البناء وهكذا انشى الطالب ايضاً، حتى نقوم بتكملة هذه الطريقة اجعل دالة البناء private وهكذا لا يستطيع اي client استدعائها ، وعلى الclient هنا هو استدعاء دالة الstatic factory التي قمت بعملها حتى ينشىء الكائن :
public class Item1 { public static void main(String[] args) { Student student2 = Student.createStudent("Ali"); System.out.println("student name: " + student2.getName()); } } class Student { private Student (String name) { this.name = name; } public static Student createStudent (String name) { return new Student(name); } public String getName() { return this.name; } private String name; }
سنجد في لغه الجافا الكثير من الكلاسات تستخدم هذا الأسلوب في انشاء الكائنات، مثل الWrapper Class (الكلاس Boolean أو Integer وغيرها من الكلاسات التي توفر خاصية الAuto-Boxing) ، مثلاً في داخل الكلاس Boolean سوف نجد أنه يتم انشائه بالشكل الأتي:
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
بالتالي يمكن لأي كلاس استخدام هذا الطريقة تسمى static factory ( لاحظ هي غير متعلقه بال Design Pattern المعروف بالاسم Factory Design Pattern) بدلاً من دالة البناء العادية ، أو يمكن تزويد الكلاس بالطريقتين، كما في مثال الطالب الأول أعلاه.
أمثلة أخرى موجودة في الجافا:
- LogManager.getLogManager
- Pattern.compile
- Collections.unmodifiableCollection , Collections.synchronizeCollection , and so on
- Calendar.getInstance
ما الفرق بين Constructor وال Static Factory
السؤال الذي قد يخطر ببالك الآن هو لماذا؟ لماذا نقوم بعمل دالة تستدعي دالة البناء ولا نقوم مباشره باستدعاء دالة البناء ؟ (أي لماذا تختار Static Factory ولماذا لم تختار ال Constructor ) ؟
لنلقى نظره أولاً على مزايا وعيوب كل من هذه الطرق في انشاء الكائنات وبالتالي سيجعلك تقرر لماذا اختار هذه عن تلك..
أحد مزايا هذه الطريقة ال Static Factory بعكس طريقة دالة البناء في أن هذه الداول لديها دوال واضحه الإسم ، وبالتالي الكود أوضح وأسهل للفهم Readability ، على سبيل المثال الكلاس BigInteger كان يحتوى على دالة بناء تستقبل عددين صحيحين وكائن من النوع Random وتقوم دالة البناء بارجاع كائن من هذا الكلاس يحتمل أن يكون عدد أولي:
BigInteger(int, int, Random)
في حالة تم استخدام طريقة الstatic factory فمن الممكن أن تأخذ الدالة شكل أوضح كثيراً كالتالي:
BigInteger.probablePrime(int, int, Random)
وهذا ما تم في نسخة الجافا 1.4 حيث أن دالة البناء الأولى لم تكن واضحه جيداً.
بعض الحالات قد تحتاج لدالة بناء بنفس تصريح دالة البناء الأولى لديك في الكلاس، ومعلوم طبعاً أنه لا يمكن أن تتواجد دالتين في الكلاس بنفس الSignature ، الحل الذي يقوم به بعض المبرمجين هو تغيير ترتيب القيم في دالة البناء الأخرى وبالتالي يتم السماح لها بالتواجد، ولكن هذا الحل يصعب استخدام الكلاس فمستخدم الكلاس عليه أن يعرف أن دالة البناء التي تستقبل الترتيب الفلاني سوف تقوم بالوظيفة التالية ودالة البناء التي تستقبل الترتيب الأخر تقوم بالوظيفة الأخرى. أما لو نظرنا للحل الأخر –static factory method- سوف نجد أنه يكل سهول يمكن تحويل دوال البناء هذه الى دوال بأسماء أوضح كثيراً.
مثال لدى كلاس يقوم بالحصول على معلومات من خلال كائن ومسار لمعلومات (ولدى طريقتين استطيع تقديم هذا المسار) ، باستخدام دالة البناء قد نصل الى :
public class GetterFoo { public GetterFoo(ImData imData, String longPath); // you can't public GetterFoo(ImData imData, String shortPath); }
كما تلاحظ أعلاه أنك لن تستطيع تطبيق الدالة الثانية بسبب تشابه الSignature في الدالتين وهذا خطأ في الترجمة ، لذلك الحل هو تغيير الترتيب:
public class GetterFoo { public GetterFoo(ImData imData, String longPath); public GetterFoo(String shortPath, ImData imData); }
بالطبع الحل يعقد الكلاينت الذي يستخدم هذا الكود ، لكن باستخدام ال Static Factory Method ، تصبح لدينا هذه القطعة الرائعه (بغض النظر عن اسماء الكلاس والمعاملات فهي مثال فقط):
public class GetterFoo { public GetterFoo getUsingLongPath(ImData imData, String path); public GetterFoo getUsingShortPath(ImData imData, String path); }
مثال أخر من كود لي في السابق، كنت اريد تحميل الرسائل من البريد والذي أحفظ معلوماته في EmailSetting ، الآن الكلاس المسؤول عن التحميل هو EmailDownloder ، واريد احياناً ان احمل البريد مع المرفقات واحياناً بدون ، هذا هو الكلاس الذي يستخدم طريقة دالة البناء:
public class EmailDownloader { public EmailDownloader(EmailSetting setting, boolean withAttachment) { } }
عندما أريد استخدام هذا الكلاس من الخارج:
// in client, what is true mean ??? EmailDownloader emailDownloder = new EmailDownloader(setting, true);
لكن لو استخدمنا طريقة ال static factory :
public class EmailDownloader { public static EmailDownloader downloadEmail(EmailSetting setting) { } public static EmailDownloader downloadEmailWithAttachment(EmailSetting setting) { } }
والكلاينت:
// in Client EmailDownloader emailDownloader = EmailDownloader.downloadEmailWithAttachment(setting);
هكذا الكود أكثر مقروئية فقط بالتغيير من دالة البناء الى ال static factory (اضافه الى ان تمرير boolean الى الدالة سواء كانت عادية أو دالة بناء خصوصاً لو كانت معامل واحد bool فقط هو من العادات التي تقلل من مقروؤية الكود ، سنتحدث عن ذلك في موضوع أخر ان شاء الله).
الميزة الأخرى لاستخدام الstatic factory بعكس دوال البناء في أنها يمكن أن لا تقوم بعمل كائن جديد في كل مرة يتم استدعائها، وهذا يسمح للكلاسات الImmutable (الكلاسات التي لا تتغير فور بناء الكائن منها مثل الكلاس String) لاعادة استخدام كائنات جاهزة مسبقاً، أو حتى حفظ Cache الكائن عند انشائه مره واحد فقط ومن ثم تمرير هذه النسخة عند كل طلب لهذا الدالة.
لو نظرنا للدالة Boolean.valueOf أعلاه لوجدناها تستخدم هذا المفهوم ،حيث هي لا تقوم بعمل كائن أبداً بل تستخدم كائن منشيء مسبقاُ في الكلاس. وهذا الأمر قد يفرق في أداء البرنامج Performance، لنفرض أن لديك كائن يستغرق وقتأ ومصادر في النظام عند انشائه وأنت تريد انشائه كثيراً فبهذه الطريقة تضمن الكائن لديك وانه تم انشائه مره واحدة فقط.
في السابق كنت اقوم بقرائه بريد ال Outlook واستخرج منه بعض المعلومات من ملف PST/OST المخزن على الجهاز، كنت اريد ان اقوم بعمل استعلامات كثيرة جداً على بعض المعلومات من هذه الرسائل، لذلك ال Caching هو الحل لأن قرائه البريد قد تستغرق 5 الى 15 ثواني (في الغالب مع الأحجام تصل الى 500 ميغا) .. لذلك تم استخدام هذه الطريقة كما يلي:
public class OutlookEmailReader { // member variables here public static List<MessageHeader> getInstance(PSTFile pst, String p) { if ( map == null || ! path.equals(p)) { caching(); } return map ; } }
هكذا داخل هذه الدالة ال Static Factory Method تستطيع التحكم Control أنت المبرمج في هل تنشيء الكائن أم تعيد استخدامه ، وعادة تسمى الكلاسات التي لديها هذه القابلية بالاسم Instance Controlled. وفي الحقيقة هناك عدة فوائد من كتابة Instance Controlled Class أولاً أنها قد تضمن أن كائنك هو كائن وحيد فقط Singleton (لا يتم انشاء الا كائن واحد فقط من الكلاس) أو أن الكلاس غير قابل لعمل كائن منه اطلاقاً Non-Insatiable (لا تستطيع عمل كائن ابداً من هذا الكلاس مثلاً الكلاس Math فقط انت تستخدم ما بداخله وغيرها من ال Utilities Classes).
الميزة الثالثه في الstatic factory method وهي بعكس دوال البناء يمكن أن ترجع كائن أبن لهذا الكلاس، وهذا يعطيك حرية في اختيار العائد من الكلاس فمثلاً يمكن اعادة كائن بدون أن يكون كلاس هذا الكائن من النوع public هذا يعني اخفاء الimplementation وتسمى الطريقة بInterface-based framework (سوف نتحدث عنها في مقاله أخرى ان شاء الله).
الميزة الرابعه لاستخدام الstatic factory هو في توضيح وتسهيل انشاء كائن من نوع معين من Generic Class، فمثلاً لانشاء Map مكونه من String و List<String> نقوم ببنائها بالشكل:
Map<String, List<String>> m = new HashMap<String, List<String>>();
لكن لو قمت بوضع Static Factory Method في الكلاس يمكن أن يكون الاستدعاء بهذا الشكل:
Map<String, List<String>> m = HashMap.newInstance();
وهذا يرجع لأن المترجم سوف يتعرف على نوع القيم من تعريف المتغير مباشره. (هذه الميزة كان يتوقع ان تضاف الى جافا 7 ولكن تم تطبيقها وبشكل أخر). وحالياً يمكنك الاستفادة منها في الكلاسات الGeneric التي تقوم بعملها.
في جافا 7 استخدم ال diamond Syntax for Generic Declarations فبدلاً من أن تقوم بعمل:
List<String> blah = new ArrayList<String>(); Map<String, String> blah = new LinkedHashMap<String, String>();
يمكنك الأن ان تستخدم أقواس فارغه Diamond Syntax وسيتم التعرف على النوع من خلال التعريف:
List<String> blah = new ArrayList<>(); Map<String, String> blah = new LinkedHashMap<>();
لجافا 7 وقفة مفصلة في المقالات التالية ان شاء الله.
من أحد الأمثلة الشائعه في ال Static Factory هو الكلاس Complex :
public class ComplexNumber { /** * Static factory method returns an object of this class. */ public static ComplexNumber valueOf(float aReal, float aImaginary) { return new ComplexNumber(aReal, aImaginary); } /** * Caller cannot see this private constructor. * * The only way to build a ComplexNumber is by calling the static * factory method. */ private ComplexNumber (float aReal, float aImaginary) { fReal = aReal; fImaginary = aImaginary; } private float fReal; private float fImaginary; //..elided }
مثل هذا الكلاس يفضل جعله Immutable ايضاً (حتى تحصل على التركيبة الرائعه Static Factory Method مع Immutable Class ، وسوف نتحدث عنها ونوضح فوائد ذلك بمقاله اخرى ان شاء الله).
الى هنا قد بينا محاسن ال Static Factory ولنتحدث قليلاً عن المساوئ فيها و هي في أنك لن تستطيع وراثه الكلاس اذا لم يحتوي على دالة بناء من التوع public أو protected ، هناك بعض الأقوال بأن ذلك يشجع المبرمج على اسخدام الCompostion بدلاً من الInheritance ولكني بالتجربة وجدت أنه أحيانا عليك في حالات عمل وراثه من كائن (حالات is-a صحيحة ) وفي هذه الحالة عليك بعمل دالة بناء protected على الأقل حتى تضمن أن تعمل دوال الstatic factory لديك.
أيضاً من الأشياء التي قد تضعفها هي عدم وجود معيار ثابت في تسمية دوال الstatic factory ، حيث قد لا يوضح هل هذه دالة عادية أم دالة بناء أم دالة static factory، على كل هناك أسماء شائعه لهذه الدوال في لغه الجافا مثل: valueOf, of, getInstance, newInstance, getType, newType
خلاصة
دوال البناء والstatic factory method لهم استخداماتهم وعليك معرفة ميزات كل منهم، وعادة يفضل استخدام الstatic factory للأسباب أعلاه، لذلك قبل أن تزود الكلاس بpublic constructor أنظر أولاً لمدى جدوى استخدام الstatic factories.
بالنسبة لي حوالي 60% من الكلاسات التي اكتبها استخدمها بها Static Factory ، فهي لها مقروئية عالية ،،
السؤال الآن للقارى ، هل استخدمتها/ستستخدمها أم مازلت ترى أن دالة البناء العادية افضل منها؟
بسم الله الرحمن الرحيم
لم استخدمها من قبل – استخدامها سوف يكون حسب البرنامج هل احتاجها ام لا بأذن الله تعالى
اكمل مواضيع اخى الفاضل متابعيين معكم بأذن الله تعالى
جزاكم الله كل خير
مقال رائع و ملئ بالمعلومات يا أخى …