Unleash the power of annotations

Briefly describe the annotations processing in java

بسم الله الرحمن الرحيم

بعد انقطاع بسيط عن كتابة بعض المواضيع .. نعود اليوم لنتعرف على واحدة من أهم المميزات التي تمتاز فيها لغة Java وقدرتها على تلبية احتياجات المطورين بشكل سريع وسلس.

يعاني الكثير من المطورين مما يعرف بالـ boilerplate code أو ما يطلق عليه بالكود المتكرر والذي سيكون من الصعب اختزاله داخل دالة نستطيع أن نستدعيها بشكل متكرر. بمعنى أنه يجب أن يتكرر الكود بطريقة أو بأخرى.

مثال على بعض هذه الاكواد الموجودة في الاندرويد :

findViewById(R.id.view)

 ( تستطيع التخلص منها عن طريق مكتبة ButterKnife إذا كنت تستخدم java. مطورو kotlin لا يحتاجون كتابة هذه الدالة أصلاً )

مثال آخر على الـ boilerplate code :

textview.setText("Example");
imageview.setDrawable(image);

(تستطيع التخلص منها عن طريق إستخدام مكتبة data binding المطورة من google)

 

كما نلاحظ من الامثلة السابقة بأن أغلب الـ boilerplate code يحل عن طريق إستخدام third-party solutions. ولكن السؤال .. هل يوجد طريقة أخرى لحل هذه المشكلة ؟

الجواب نعم .. عن طريق ما يسمى بالـ annotations. وجدت الـ annotations حتى تسهل على المطور إعادة استخدام بعض أكواد الـ boilerplate بحيث يستطيع المطور عن طريق كتابة سطر واحد فقط أن يأمر الـ compiler بإضافة أكواد أضافية لا نراها ولكنها موجودة. هذا يساعدنا في تسريع عملية الكتابة بالإضافة إلى تقليل الـ boilerplate code وزيادة الـ readability للبرنامج بشكل عام.

قد تستخدم الـ annotations أيضاً لتقليل إحتمالية وقوع الخطأ، بحيث يستطيع المطور إستخدام بعض الـ pre-defined annotations والتي توفرها لك java بشكل جاهز. ولعل أهم مثال على هذا النوع هو @Override وهي واحدة من أشهر الـ annotations. تستخدم @Override قبل كتابة الدوال بشكل عام للدلالة على ان هذه الدالة موروثة ويتم إعادة تعريفها داخل الـ child class. بمجرد إضافتك للـ @Override على method جديدة ليست موجودة في الـ parent class سيعطيك الـ compiler خطأ مباشر.

أيضاً قيامك بوضع @NonNull قبل تعريف المتغيرات سيعطي الـ compiler القدرة على تنبيهك بشكل سريع في حال قيامك بعمل assign بقيمة null لهذا المتغير.

أغلب المكتبات الموجودة داخل الأندرويد والمستخدمة من كثير من المطورين تعتمد بشكل كبير على ميزة الـ annotation لتأخذ عنك عبء كتابة الأكواد الطويلة وتسهيل مراجعة وقراءة الأكواد. من أشهر الأمثلة على ذلك ما تقدمه مكتبة ButterKnife لمطوري تطبيقات الاندرويد باستخدام لغة java. بكتابة @BindView قبل الـ view object ستقوم المكتبة بربط المتغير مع الـ view وكتابة findViewById بشكل خفي بدون أن يٌكتب سطر واحد داخل كلاس الـ Activity أو الـ Fragment.

التحدي الأكبر الآن هو قيام المطور بتطوير annotation خاصة فيه لتقوم بحل مشكلة خاصة بالبرنامج الذي يقوم بتطويره. وهذا ما سنقوم به في موضوع اليوم. الموضوع قد يكون متشعب وطويل. (أنصح بأنك تكون راااايق :)

لنبدأ ..

أولاً وقبل الحديث في جانب الأكواد .. لنتعرف أولاً على أنواع هذه الـ annotations. ويتم تقسيم الـ annotations إلى قسمين بحسب:

- مكان كتابة الـ annotation ( القدرة على كتابة الـ annotation قبل الدوال أو المتغيرات أو الكلاسات أو في أكثر من مكان في نفس الوقت ) @Target

ويوجد ما يقارب عشرة أماكن بإمكانك إختيارها لوضع الـ annotation الخاصة بك. أهمها :

  • Type أي قبل الـ class, interface, enum وغيرها مثل (@Entity الخاصة بمكتبة Room على الأندرويد)
  • Field أي قبل المتغيرات مثل @NonNull
  • Method أي قبل الدوال مثل @Override
  • Parameter أي قبل مدخلات الدوال مثل @Path @Field في مكتبة Retrofit
  • Constructor أي قبل الكونستركتر

- طريقة عمل الـ complier مع هذه الـ annotations وهل سيتم تخزينها أو لا ( هل ستؤثر هذه الـ annotations في عمل البرنامج أم لا ) @Retention

  • Source – لن يتم تخزينها ولا يوجد أي تأثير لهذه الـ annotations على البرنامج ( لن تعمل أثناء الـ runtime)
  • Class – سيتم تخزينها داخل ملف الكلاس ولكن لا يوجد لها تأثير أيضاً ( لن تعمل أثناء الـ runtime )
  • Runtime – سيتم تخزينها وسيتم معالجتها أيضاً من قبل الـ compiler لمعرفة ما إذا كانت تؤثر على الأكواد المكتوبة.

تستخدم أنواع الـ source والـ class غالباً لإعطاء ما يعرف بالـ metadata عن الاكواد الخاصة بك. وغالباً ما تكون محدودة ومستخدمة من قبل المطور فقط.

لنرى كيف يعمل هذا النوع من الـ annotations.

( إذا كنت تعمل على IDE غير IntelliJ أو Android Studio فيجب عليك تحميل الـ Gradle Plugin الخاصة بالـ IDE الذي تعمل عليه ) سأقوم في هذا الموضوع بالعمل على Android Studio كونه يتميز بوجود الـ Gradle بشكل مباشر ولكن يمكنك بالطبع إستخدام محررات أخرى.

لنبدأ  الان بالجانب العملي ..

- أولاً .. نقوم بإنشاء بروجكت جافا جديد ونسميه Annotation على سبيل المثال.

 

- ثانياً .. نقوم الان بإنشاء كلاس جديد بإسم Truck على سبيل المثال وتعبئته بالدوال والمتغيرات المطلوبة.

public class Truck {

    private int mSpeed;
    private String mName;

    public Truck(int mSpeed, String mName) {
        this.mSpeed = mSpeed;
        this.mName = mName;
    }

    public int getSpeed() {
        return mSpeed;
    }

    public String getName(){
        return mName;
    }
}

 

- ثالثاً .. نقوم الان بإنشاء Interface جديد باسم الـ annotation المراد تطويرها. سنقوم بإختيار إسم Metadata.

- رابعاً .. نقوم بوضع علامة @ قبل كلمة interface حتى نخبر الـ compiler بأن هذا الـ interface سيتم استخدامه كـ annotation.

public @interface Metadata { }

 

- خامساً .. نقوم بتحديد الـ Target والـ Retention والتي سبق وذكرناها سابقاً. سنقوم بتطوير هذه الـ annotation لكي توضع قبل الدوال ولذلك سيتم إختيار ElementType.METHOD. بإمكانك وضع أكثر من Target على نفس الـ annotation. وسيتم إختيار الـ Retention لكي تكون RUNTIME وسنرى الفرق بعد قليل بين اختيار Retentions مختلفة.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Metadata {}

 

- سادساً .. نقوم بتعريف بعض المتغيرات والتي ستعمل كـ Metadata ووظيفتها إعطاؤنا معلومات أكثر عن الدوال المشمولة داخل الـ annotation. قمت بتعريف author, last_update, description. بإمكانك وضع default value لواحدة أو أكثر من المتغيرات. 

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Metadata {

    String author() default "Agha";
    String last_update();
    String description();

}

 

- سابعاً .. نقوم الان بالرجوع إلى Truck وإضافة هذه الـ annotation بالمعلومات الخاصة بها قبل كل دالة. نلاحظ الفائدة التي من الممكن أن توفرها الـ annotations في هذا المثال .. وخصوصاً عندما يعمل مطورين أو أكثر على نفس الكلاس.

public class Truck {

    private int mSpeed;
    private String mName;

    public Truck(int mSpeed, String mName) {
        this.mSpeed = mSpeed;
        this.mName = mName;
    }

    @Metadata(last_update = "27/3/2019", description = "returns speed")
    public int getSpeed() {
        return mSpeed;
    }

    @Metadata(author = "Ahmad", last_update = "25/3/2019", description = "returns name")
    public String getName(){
        return mName;
    }
}

 

- ثامناً .. نقوم بإنشاء Driver class لنرى كيف من الممكن الاستفادة منطقياً من هذه الـ annotation وهل سيستطيع الـ compiler التعرف عليها أم لا. للتعرف على المعلومات الخاصة بالدوال والتي تم كتابتها مسبقاً داخل metadata annotation سنقوم بتعريف متغير من نوع Truck.class. نقوم فيما بعد بالبحث داخل الكلاس عن الدوال المعنونة بـ @Metadata ونستخرج المعلومات ونطبعها للتأكد من انها تعمل بشكل صحيح.

public class Driver {

    public static void main(String [] args){

        Class car = Truck.class;

        for (Method method : car.getMethods()){

            Metadata annotation = method.getAnnotation(Metadata.class);

            if (annotation != null){ // not null
                System.out.println("Method : " + method.getName());
                System.out.println("Author : " + annotation.author());
                System.out.println("Description : " + annotation.description());
                System.out.println("Last update : " + annotation.last_update());

                System.out.println();
            } // if

        } // for

    }

}

 

نقوم بتشغيل البرنامج ونلاحظ المعلومات المطبوعة داخل الـ console

 

- تاسعاً .. نقوم بالان بتغيير الـ Retention إلى Source أو Class وسنلاحظ بأن الكونسول لن يطبع أي سطر والسبب في أن الـ compiler بمجرد تغيير الـ Retention إلى Class أو Source لن يدرج الـ annotation داخل عملية الـ Runtime وسيكون مخرج method.getAnnotation(Metadata.class) دائماً null.

 

كان هذا مثالاً بسيطاً لطريقة إستخدام الـ annotations في عمليات بسيطة جداً. ولكن ماذا عن العمليات المعقدة ؟ ماذا لو أردنا القيام ببناء annotation قوية قادرة على التحكم بمنطق البرنامج بل قادرة أيضاً على كتابة بعض الأكواد وإنشاء كلاسات بطريقة مؤتمتة وسريعة بدون تدخل المطور نفسه. هذا الأمر ممكن باستخدام واحدة من أفضل المكاتب المتخصصة في هذا المجال وهي JavaPeot بالإضافة إلى مساعدة بسيطة من مكتبة Google Guava. تقوم هذه المكتبين بإعطاء المطور القدرة على كتابة كلاسات وحفظها وتشغيلها بطريقة غير مباشرة. لنأخذ هذا المثال البسيط :

بإمكاننا كتابة كلاس HelloWorld بهذه الطريقة الاعتيادية

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

 

ولكن بإمكاننا أيضاً كتابته بهذه الطريقة 

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

 

وهنا نلاحظ قوة مكتبة Java peot حيث تمكنت من كتابة هذا الكلاس بطريقة شبه يدوية.

شرح الكود :

- طريقة بناء الكلاسات يفضل أن تكون باستخدام Bottom-up approach .. سوف نبدأ أولاً ببناء الـ main method عن طريق إستخدام MethodSpec كلاس وهو كلاس يستخدم لبناء الدوال. 

- نقوم ببناء الكلاس باستخدام TypeSpec وإضافة الدوال عليه بشكل يدوي.

- نقوم أخيراً بكتابة الكلاس في ملف جديد عن طريق إستخدام JavaFile. 

 

باستخدام هذه المكتبة وبالإضافة إلى العمليات التي تقدمها لنا الـ annotations نستطيع بناء annotations قادرة على إنشاء كلاسات جديدة بطريقة اوتوماتيكية. المثال الذي سنعمل عليه الان سيكون واحد من الامثلة المشهورة وهو إنشاء Singleton Class. كنوع من المراجعة .. لعمل كلاس Singleton نحتاج لتوفر ثلاثة أشياء :

  1. Private constructor
  2. Private static instance يملك datatype مماثلة للـ singleton class
  3. Public static method تقوم بإعطاء الـ private instance

إعادة كتابة هذه الشروط لكل Singleton Class موجود لدينا عملية طويلة ويمكن أتمتها بسهولة. بإمكاننا إنشاء custom annotation بإسم @Singleton بحيث تخبر الـ java compiler بأننا نريد هذا الـ class أن يكون singleton. كيف ؟

قبل البدء في الجانب العملي والتطبيقي أنصح بقراءة الـ java peot documentation كوننا لن نقوم بشرح وتكرار بعض تفاصيل أكواد المكتبة المشروحة من قبل. يمكنك قراءة تفاصيل المكتبة وشروحات الأكواد من هنا

للبدء في الجانب التطبيقي .. يجب علينا أولاً فهم الطريقة التي سنعمل بها. سنقوم بإنشاء ثلاثة Modules حتى نطبق مفهوم الـ separation of concerns بحيث يكون كل module مسؤول عن مهمة معينة. هذا يعطينا إمكانية إعادة استخدام هذه الـ modules لاغراض أخرى فيما بعد في برامج أخرى بالإضافة إلى تقليل الاكواد وتعقيداتها.

الـ module الأول سيكون الـ app module أو الـ module الذي سيكتب فيه كلاسات البرنامج العادية.

الـ module الثاني سيكون الـ annotations module أو الـ module الذي ستحفظ فيه الـ custom annotations المراد استخدامها داخل الـ app module.

الـ module الثالث سيكون الـ annotation processor module وهو الـ module المسؤول عن الجانب المنطقي لكل annotation مكتوبة داخل الـ annotations module.

 

طريقة الـ execution والتواصل بين الثلاث modules ستكون كالتالي:

- عند عملية الـ compilation سيقوم الـ compiler بقراءة الـ annotation فوق أحد الكلاسات بإسم Singleton.

- الـ compiler لا يعرف آلية التصرف في ظل وجود هذه الـ annotation الغريبة وسيقوم مباشرة بإحالة المشكلة إلى annotation processor.

- سيقوم الـ annotation processor بالتعرف على @Singleton والتأكد ما إذا كانت هذه الـ annotation موجودة داخل annotation module أو لا. إذا كانت موجودة سيبدأ بالعمل مباشرة. هذه هي آلية التنفيذ بكل اختصار.

سنبدأ أولاً بأسهل module وهو annotation module وفيه سنخزن كل الـ custom annotation والتي في مثالنا ستكون @Singleton.

- أولاً .. نقوم بإنشاء java module جديد ونسميه annotation.

 

- ثانياً .. نقوم بإنشاء interface جديد بإسم Singleton. نضيف علامة الـ @  ونحدد الـ target والـ retention. نلاحظ بأنه تم تغيير الـ target إلى Type كوننا نريد وضع هذه الـ annotation فوق الكلاسات وليس الدوال.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton { }

بهذه الخطوة نكون انتهينا من الـ annotation module.

 

نبدأ الآن بالـ annotation processor module.

- أولاً .. نقوم بإنشاء java module جديد ونسميه annotationProcessor بنفس الخطوات الماضية.

- ثانياً .. نقوم بإضافة هذه الـ dependencies إلى الـ module. (تأكد من تواجد الـ gradle plugin داخل مشروعك حتى تتمكن من عمل import للمكتبات المطلوبة). نلاحظ وجود الـ annotations module من ضمن الـ dependencies والسبب ذكرناه سابقاً .. للتأكد من أن الـ annotation المرسل من الـ app module لمعالجته موجود داخل الـ annotation module.

بعد انتهاء تحميل المكتبات، نكون جاهزين للعمل على annotationProcessor Module.

 

- ثالثاً .. نقوم بإنشاء كلاس جديد بإسم SingletonProcessor. هذا الكلاس سيستدعى من قبل الـ compiler عند رؤية @Singleton داخل أي من كلاسات app module.

- رابعاً .. نقوم بعمل extend لكلاس AbstractProcessor وعمل override لهذه الأربع دوال.

@Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

 

- خامساً .. نقوم بإنشاء متغيرين من نوع Filer ومن نوع Elements. الفائدة من Filer هي القدرة على إنشاء الكلاس بطريقة يدوية وحفظه داخل المشروع. (أي أننا سنكون قادرين على الدخول على الكلاس المنشأ من قبل الـ processor وتعديله أيضاً). Elements سنحتاجها لمعرفة الـ type المحدد بـ @Singleton هل هو كلاس أم متغير أم دالة.

private Filer filer;
private Elements elements;

 

- سادساً .. نقوم بتعريف المتغيرات داخل دالة init بالاضافة إلى تعديل دالة getSupportedAnnotationTypes وفيها سيتم تحديد الـ annotation التابعة لهذا الكلاس ( في مثالنا ستكون Singleton) بالإضافة إلى دالة getSupportedSourceVersion.

public class SingletonProcessor extends AbstractProcessor {

    private Filer filer;
    private Elements elements;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
        elements = processingEnvironment.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return ImmutableSet.of(Singleton.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

 

- سابعاً .. نأتي الآن لأطول دالة وهي دالة process وهي الدالة الرئيسية التي ستحوي كل الـ logic المتعلق بمعالجة @Singleton.

1- نقوم أولاً بالمرور على الـ elements الموجود داخل الكلاس المعنون بـ [email protected] هذه الـ elements تشمل المتغيرات والدوال والكلاس نفسه والباكج وغيرها.

for (Element element : roundEnvironment.getElementsAnnotatedWith(Singleton.class)) { }

الفكرة من هذا الـ loop هو معرفة الـ class element كوننا نعلم مسبقاً بأن [email protected] يجب أن توضع فوق الكلاسات فقط. 

if (element.getKind() != ElementKind.CLASS) {
     System.out.println("Can only be applied to class.");
     return true;
}

 

2- بعد التأكد من أن الـ element عبارة عن كلاس. نبدأ بعملية إنشاء كلاس الـ Singleton. نبدأ أولاً بالـ private constructor.

// private constructor
   MethodSpec constructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PRIVATE)
                .build();

 

3 - ومن ثم الـ singleton object

// singleton object
            TypeElement typeElement = (TypeElement) element;
            CodeBlock codeBlock = CodeBlock.builder().add("null").build();
            String varName = element.getSimpleName().toString().toLowerCase();

            FieldSpec singletonObject = FieldSpec.builder(
                    ClassName.get(typeElement),
                    varName,
                    Modifier.PRIVATE,
                    Modifier.STATIC)
                    .initializer(codeBlock)
                    .build();

 

4- ومن ثم الـ singleton class ( الـ SINGLETON_CLASS عبارة عن string ليميز الكلاس العادي المعنون بـ @Singleton عن الكلاس الـ singleton المراد إنشاؤه. بمعنى آخر .. لو كان الكلاس المعنون باسم Student .. السنجلتون كلاس سيكون باسم Student_Singleton

// singleton class
            TypeSpec singletonClass = TypeSpec.classBuilder(element.getSimpleName().toString()+SINGLETON_CLASS)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructor)
                    .addMethod(getInstanceMethod)
                    .addField(singletonObject)
                    .build();

 

5- أخيراً نقوم بكتابة أمر إنشاء الكلاس وحفظه داخل المشروع. (الـ PREFIX هو com.example)

// write class
            try {
                JavaFile.builder(PREFIX + singletonClass.name.toLowerCase(), singletonClass)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }

 

ليكون الشكل النهائي لكلاس SingletonProcessor كالتالي ( لا تنسى إضافة AutoService فوق الكلاس حتى يتعرف الـ complier على هذا الكلاس كـ processor ) :

@AutoService(Processor.class)
public class SingletonProcessor extends AbstractProcessor {

    private Filer filer;
    private Elements elements;

    private final String PREFIX = "com.exmaple.";
    private final String SINGLETON_CLASS = "_Singleton";

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
        elements = processingEnvironment.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {


        for (Element element : roundEnvironment.getElementsAnnotatedWith(Singleton.class)) {

            if (element.getKind() != ElementKind.CLASS) {
                System.out.println("Can only be applied to class.");
                return true;
            }

            // private constructor
            MethodSpec constructor = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PRIVATE)
                    .build();

            // singleton object
            TypeElement typeElement = (TypeElement) element;
            CodeBlock codeBlock = CodeBlock.builder().add("null").build();
            String varName = element.getSimpleName().toString().toLowerCase();

            FieldSpec singletonObject = FieldSpec.builder(
                    ClassName.get(typeElement),
                    varName,
                    Modifier.PRIVATE,
                    Modifier.STATIC)
                    .initializer(codeBlock)
                    .build();

            // singleton method
            MethodSpec getInstanceMethod = MethodSpec.methodBuilder("getInstance")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(ClassName.get(typeElement))
                    .beginControlFlow("if ($N == null)", varName)
                    .addStatement("$N = new $T()", varName, ClassName.get(typeElement))
                    .endControlFlow()
                    .addStatement("return $N", varName)
                    .build();

            // singleton class
            TypeSpec singletonClass = TypeSpec.classBuilder(element.getSimpleName().toString()+SINGLETON_CLASS)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructor)
                    .addMethod(getInstanceMethod)
                    .addField(singletonObject)
                    .build();


            // write class
            try {
                JavaFile.builder(PREFIX + singletonClass.name.toLowerCase(), singletonClass)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }


        }
        return true;
    }



    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return ImmutableSet.of(Singleton.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

 

قبل التجربة، نحتاج لإضافة الـ annotation module والـ annotation processor module داخل الـ app moudule حتى يتعرف الـ app على الـ annotations الجديدة.

// annotations
    implementation project(':annotations')
    annotationProcessor project(':annotationProcessor')

 

كل شي جاهز. سنقوم الان بانشاء كلاس جديد باسم Student ونقوم بإضافة الـ attibutes الخاصة به.

public class Student {

    private String name;
    private String major;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getMajor() {
        return major;
    }

    public void setMajor(String major) {
        this.major = major;
    }
}

 

الان سنقوم بإضافة @Singleton فوق كلاس Student.

@Singleton
public class Student { }

 

ونقوم بعمل Rebuild للمشروع.

 

نلاحظ وجود ملف جديد باسم genetatedJava ويحتوي بداخله كلاس الـ Student_Singleton.

 

نقوم الان بتجربته وملاحظة ما اذا كان يعمل بشكل صحيح.

public static void main(String [] args){

        Student student1 = Student_Singleton.getInstance();
        Student student2 = Student_Singleton.getInstance();

        student1.setName("Agha");
        student2.setName("Ahmad");

        student1.setMajor("SWE");
        student2.setMajor("ICS");

        System.out.println("Name : " + Student_Singleton.getInstance().getName());
        System.out.println("Major : " + Student_Singleton.getInstance().getMajor());

    }

 

الـ output. ( شغاااال :))

 

ملاحظة هامة : لو نلاحظ بأننا ما زلنا نستطيع استخدام كلاس Student ولم يتم تحويله بشكل مطلق إلى Singleton Class وإنما تم إنشاء كلاس آخر يحمل singleton object من نوع student ليكون هو الـ singleton class.

 

إلى هنا نكون قد انتهينا من الموضوع. ونراكم في مواضيع أخرى إن شاء الله :)

 

المصادر : Youtube - Java Docs - Medium

 

3
إعجاب
1056
مشاهدات
0
مشاركة
1
متابع

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !