الفصل 1 مرحبًا، تعبير لامدا!
القسم 1
يواجه أسلوب ترميز Java تغيرات هائلة.
سيصبح عملنا اليومي أبسط وأكثر ملاءمة وأكثر تعبيراً. جافا، طريقة برمجة جديدة، ظهرت في لغات برمجة أخرى منذ عقود. بعد تقديم هذه الميزات الجديدة إلى Java، يمكننا كتابة تعليمات برمجية أكثر إيجازًا وأنيقًا وأكثر تعبيرًا وبها أخطاء أقل. يمكننا تنفيذ استراتيجيات وأنماط تصميم مختلفة باستخدام كود أقل.
في هذا الكتاب سوف نستكشف أسلوب البرمجة الوظيفية من خلال أمثلة من البرمجة اليومية. قبل استخدام هذه الطريقة الجديدة والأنيقة للتصميم والبرمجة، دعنا أولاً نلقي نظرة على ما هو جيد جدًا فيها.
غيرت طريقة تفكيرك
الأسلوب الحتمي - هذا هو النهج الذي قدمته لغة جافا منذ بدايتها. باستخدام هذا الأسلوب، علينا أن نخبر Java بما يجب فعله في كل خطوة، ثم نشاهدها وهي تنفذها خطوة بخطوة. وهذا أمر جيد بالطبع، لكنه يبدو بدائيا بعض الشيء. يبدو الكود مطولًا بعض الشيء، ونتمنى أن تصبح اللغة أكثر ذكاءً قليلًا؛ يجب علينا فقط أن نخبرها بما نريد بدلاً من إخبارها بكيفية القيام بذلك. ولحسن الحظ، يمكن أن تساعدنا Java أخيرًا في تحقيق هذه الرغبة. دعونا نلقي نظرة على بعض الأمثلة لفهم مزايا واختلافات هذا النمط.
الطريقة العادية
لنبدأ بمثالين مألوفين. هذه طريقة أوامر للتحقق مما إذا كانت شيكاغو موجودة في مجموعة المدينة المحددة - تذكر أن الكود المدرج في هذا الكتاب ليس سوى جزء جزئي.
انسخ رمز الكود كما يلي:
تم العثور على قيمة منطقية = خطأ؛
ل(مدينة السلسلة: المدن) {
إذا (city.equals("شيكاغو")) {
وجدت = صحيح؛
استراحة؛
}
}
System.out.println("هل وجدت شيكاغو؟:" + Found);
تبدو هذه النسخة الحتمية مطولة وبدائية بعض الشيء، وهي مقسمة إلى عدة أجزاء تنفيذية. أولاً، قم بتهيئة علامة منطقية تسمى "تم العثور عليها"، ثم قم باجتياز كل عنصر في المجموعة إذا تم العثور على المدينة التي نبحث عنها، فقم بتعيين هذه العلامة، ثم اقفز خارج الحلقة وأخيرًا اطبع نتائج البحث؛
طريقة أفضل
بعد قراءة هذا الكود، سيفكر مبرمجو Java المهتمون بسرعة في طريقة أكثر إيجازًا ووضوحًا، مثل هذا:
انسخ رمز الكود كما يلي:
System.out.println("هل وجدت شيكاغو؟:" +city.contains("شيكاغو"));
يعد هذا أيضًا أسلوبًا حتميًا في الكتابة - فالطريقة التي تحتوي عليها تفعل ذلك لنا مباشرةً.
التحسينات الفعلية
كتابة التعليمات البرمجية مثل هذا لها العديد من المزايا:
1. لا مزيد من العبث بهذا المتغير القابل للتغيير
2. قم بتغليف التكرار في الطبقة السفلية
3. الكود أبسط
4. الكود أكثر وضوحًا وتركيزًا
5. اتخذ عدداً أقل من التحويلات وادمج التعليمات البرمجية واحتياجات العمل بشكل أوثق
6. أقل عرضة للخطأ
7. سهل الفهم والصيانة
لنأخذ مثالا أكثر تعقيدا.
هذا المثال بسيط جدًا، حيث يمكن رؤية الاستعلام عما إذا كان العنصر موجودًا في المجموعة في كل مكان في Java. لنفترض الآن أننا نريد استخدام البرمجة الحتمية لتنفيذ بعض العمليات الأكثر تقدمًا، مثل تحليل الملفات، والتفاعل مع قواعد البيانات، واستدعاء خدمات الويب، والبرمجة المتزامنة، وما إلى ذلك. الآن يمكننا استخدام Java لكتابة تعليمات برمجية أكثر إيجازًا وأنيقة وخالية من الأخطاء، وليس فقط في هذا السيناريو البسيط.
الطريقة القديمة
دعونا ننظر إلى مثال آخر. نحدد نطاقًا من الأسعار ونحسب السعر الإجمالي المخفض بطرق مختلفة.
انسخ رمز الكود كما يلي:
أسعار القائمة النهائية <BigDecimal> = Arrays.asList(
BigDecimal الجديد("10")، BigDecimal الجديد("30")، BigDecimal الجديد("17")،
BigDecimal الجديد("20")، BigDecimal الجديد("15")، BigDecimal الجديد("18")،
new BigDecimal("45"), new BigDecimal("12"));
لنفترض أن هناك خصم 10٪ إذا تجاوز 20 يوانًا، فلننفذه بالطريقة العادية أولاً.
انسخ رمز الكود كما يلي:
BigDecimal TotalOfDiscountedPrices = BigDecimal.ZERO;
ل(سعر BigDecimal: الأسعار) {
إذا (price.compareTo(BigDecimal.valueOf(20)) > 0)
TotalOfDiscountedPrices =
TotalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("إجمالي الأسعار المخفضة: " + TotalOfDiscountedPrices);
يجب أن يكون هذا الرمز مألوفًا جدًا؛ استخدم أولاً متغيرًا لتخزين السعر الإجمالي، ثم قم بالتكرار عبر جميع الأسعار، وابحث عن الأسعار التي تزيد عن 20 يوانًا، واحسب أسعارها المخفضة، وأضفها إلى السعر الإجمالي في النهاية؛ السعر بعد الخصم .
هنا هو إخراج البرنامج:
انسخ رمز الكود كما يلي:
إجمالي الأسعار المخفضة: 67.5
النتيجة صحيحة تمامًا، لكن الكود فوضوي بعض الشيء. ليس خطأنا أننا لا نستطيع الكتابة إلا بالطريقة التي لدينا. ومع ذلك، فإن هذا الرمز بدائي بعض الشيء، فهو لا يعاني من نوع جنون العظمة الأساسي فحسب، بل ينتهك أيضًا مبدأ المسؤولية الفردية. إذا كنت تعمل من المنزل ولديك أطفال يريدون أن يصبحوا مبرمجين، فيجب عليك إخفاء الكود الخاص بك، في حال رأوه وتنهدوا بخيبة أمل وقالوا: "هل تكسب عيشك من خلال القيام بذلك؟"
هناك طريقة أفضل
يمكننا أن نفعل ما هو أفضل - وأفضل بكثير. الكود الخاص بنا يشبه إلى حد ما مواصفات المتطلبات. وهذا يمكن أن يؤدي إلى تضييق الفجوة بين متطلبات العمل والتعليمات البرمجية المطبقة، مما يقلل من احتمالية سوء تفسير المتطلبات.
لم نعد نسمح لـ Java بإنشاء متغير وتعيينه إلى ما لا نهاية، بل نحتاج إلى التواصل معه من مستوى أعلى من التجريد، مثل الكود التالي.
انسخ رمز الكود كما يلي:
إجمالي BigDecimal النهائيOfDiscountedPrices =
الأسعار. تيار ()
.filter(price ->price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(السعر -> السعر.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("إجمالي الأسعار المخفضة: " + TotalOfDiscountedPrices);
اقرأها بصوت عالٍ - قم بتصفية الأسعار التي تزيد عن 20 يوانًا، وقم بتحويلها إلى أسعار مخفضة، ثم قم بإضافتها. هذا الرمز هو تمامًا نفس العملية التي استخدمناها لوصف متطلباتنا. في Java، من السهل أيضًا طي سطر طويل من التعليمات البرمجية ومحاذاته سطرًا وفقًا للنقطة الموجودة أمام اسم الطريقة، تمامًا كما هو مذكور أعلاه.
الكود بسيط جدًا، ولكننا نستخدم العديد من الأشياء الجديدة في Java8. أولاً، نسمي طريقة التدفق لقائمة الأسعار. وهذا يفتح الباب أمام عدد لا يحصى من التكرارات الملائمة، والتي سنناقشها لاحقًا.
نحن نستخدم بعض الأساليب الخاصة، مثل التصفية والخريطة، بدلاً من اجتياز القائمة بأكملها مباشرةً. هذه الأساليب ليست مثل تلك الموجودة في JDK التي استخدمناها من قبل، فهي تقبل دالة مجهولة - تعبير لامدا - كمعلمة. (وسوف نناقش هذا بعمق لاحقًا). نطلق على طريقة التخفيض () حساب مجموع الأسعار التي يتم إرجاعها بواسطة طريقة الخريطة ().
تمامًا مثل الطريقة التي تحتوي على، يكون جسم الحلقة مخفيًا. ومع ذلك، فإن طريقة الخريطة (وطريقة التصفية) أكثر تعقيدًا. فهو يستدعي تعبير لامدا الذي تم تمريره لحساب كل سعر في قائمة الأسعار، ويضع النتيجة في مجموعة جديدة. أخيرًا نطلق على طريقة الاختزال هذه المجموعة الجديدة للحصول على النتيجة النهائية.
هذا هو إخراج الكود أعلاه:
انسخ رمز الكود كما يلي:
إجمالي الأسعار المخفضة: 67.5
مجالات التحسين
يعد هذا تحسنًا كبيرًا مقارنة بالتنفيذ السابق:
1. منظمة بشكل جيد ولكن ليست مزدحمة
2. لا توجد عمليات على مستوى منخفض
3. من السهل تعزيز أو تعديل المنطق
4. التكرار حسب مكتبة الطريقة
5. تقييم كسول لجسم الحلقة
6. توازي بسهولة
أدناه سنتحدث عن كيفية تنفيذ Java لهذا.
تعبيرات لامدا موجودة هنا لإنقاذ العالم
تعبيرات Lambda هي اختصار ينقذنا من مشاكل البرمجة الحتمية. لقد غيرت هذه الميزة الجديدة التي قدمتها Java أسلوب البرمجة الأصلي لدينا، مما يجعل التعليمات البرمجية التي نكتبها ليست فقط موجزة وأنيقة وأقل عرضة للخطأ، ولكنها أيضًا أكثر كفاءة وسهلة التحسين والتحسين والتوازي.
القسم 2: أكبر مكاسب من البرمجة الوظيفية
يحتوي رمز النمط الوظيفي على نسبة إشارة إلى ضوضاء أعلى؛ تتم كتابة تعليمات برمجية أقل، ولكن يتم عمل المزيد لكل سطر أو تعبير. بالمقارنة مع البرمجة الحتمية، فقد أفادتنا البرمجة الوظيفية كثيرًا:
يتم تجنب التعديل الصريح أو تعيين المتغيرات، والتي غالبًا ما تكون مصدر الأخطاء وتجعل من الصعب موازاة التعليمات البرمجية. في برمجة سطر الأوامر، نقوم باستمرار بتعيين قيم للمتغير TotalOfDiscountedPrices في نص الحلقة. في النمط الوظيفي، لم يعد الكود يخضع لعمليات تعديل صريحة. كلما قل عدد المتغيرات التي تم تعديلها، قل عدد الأخطاء الموجودة في الكود.
يمكن موازنة رمز النمط الوظيفي بسهولة. إذا كانت العملية الحسابية تستغرق وقتًا طويلاً، فيمكننا بسهولة تشغيل عناصر القائمة بشكل متزامن. إذا أردنا موازنة التعليمات البرمجية الحتمية، فيجب علينا أيضًا القلق بشأن المشكلات الناجمة عن التعديل المتزامن لمتغير TotalOfDiscountedPrices. في البرمجة الوظيفية، لا نصل إلى هذا المتغير إلا بعد معالجته بالكامل، وبالتالي القضاء على المخاوف المتعلقة بسلامة الخيط.
الكود أكثر تعبيرا. تنقسم البرمجة الحتمية إلى عدة خطوات لشرح ما يجب فعله - إنشاء قيمة تهيئة، والتكرار عبر الأسعار، وإضافة أسعار الخصم إلى المتغيرات، وما إلى ذلك - بينما تحتاج البرمجة الوظيفية فقط إلى أن تقوم طريقة الخريطة الخاصة بالقائمة بإرجاع قيمة بما في ذلك الخصم ما عليك سوى إنشاء قائمة جديدة بالأسعار ثم تجميعها.
البرمجة الوظيفية أبسط؛ هناك حاجة إلى تعليمات برمجية أقل لتحقيق نفس النتيجة مقارنة بالبرمجة الحتمية. الكود الأنظف يعني كودًا أقل للكتابة، وأقل للقراءة، وأقل للصيانة - راجع "هل أقل إيجازًا بما يكفي ليكون موجزًا في الصفحة 7؟"
يعد الكود الوظيفي أكثر سهولة - فقراءة الكود تشبه وصف المشكلة - ويسهل فهمه بمجرد أن نتعرف على بناء الجملة. تقوم طريقة الخريطة بتنفيذ الوظيفة المحددة (حساب سعر الخصم) لكل عنصر من عناصر المجموعة، ثم تقوم بإرجاع مجموعة النتائج، كما هو موضح في الشكل أدناه.
الشكل 1 - تؤدي الخريطة الوظيفة المحددة على كل عنصر في المجموعة
باستخدام تعبيرات لامدا، يمكننا إطلاق العنان لقوة البرمجة الوظيفية في Java. باستخدام النمط الوظيفي، يمكنك كتابة تعليمات برمجية أكثر تعبيرًا وإيجازًا ولها مهام أقل وأخطاء أقل.
يعد دعم البرمجة الموجهة للكائنات ميزة رئيسية لـ Java. البرمجة الوظيفية والبرمجة الشيئية لا يستبعد أحدهما الآخر. التغيير الحقيقي في الأسلوب هو من برمجة سطر الأوامر إلى البرمجة التعريفية. في Java 8، يمكن دمج الوظائف والكائنات بشكل فعال. يمكننا الاستمرار في استخدام أسلوب OOP لنمذجة كيانات المجال وحالاتها وعلاقاتها. بالإضافة إلى ذلك، يمكننا أيضًا استخدام الوظائف لنمذجة السلوك أو انتقالات الحالة وسير العمل ومعالجة البيانات وإنشاء وظائف مركبة.
القسم 3: لماذا نستخدم الأسلوب الوظيفي؟
لقد رأينا مزايا البرمجة الوظيفية، ولكن هل يستحق استخدام هذا النمط الجديد؟ هل هذا مجرد تحسن بسيط أم تغيير كامل؟ لا تزال هناك العديد من الأسئلة العملية التي تحتاج إلى إجابة قبل أن نقضي الوقت في هذا الأمر.
انسخ رمز الكود كما يلي:
سأل شياو مينغ:
هل الرمز الأقل يعني البساطة؟
البساطة تعني تقليل الفوضى ولكن ليس في التحليل النهائي، فهي تعني القدرة على التعبير عن النية بشكل فعال. الفوائد بعيدة المدى.
إن كتابة التعليمات البرمجية تشبه تجميع المكونات معًا، فالبساطة تعني القدرة على مزج المكونات في التوابل. تتطلب كتابة التعليمات البرمجية المختصرة عملاً شاقاً. هناك تعليمات برمجية أقل للقراءة، والتعليمات البرمجية المفيدة حقًا شفافة بالنسبة لك. الرمز القصير الذي يصعب فهمه أو الذي يخفي التفاصيل يكون قصيرًا وليس موجزًا.
الكود البسيط يعني في الواقع تصميمًا رشيقًا. كود بسيط بدون الروتين. وهذا يعني أنه يمكننا تجربة الأفكار بسرعة، والمضي قدمًا إذا كانت تعمل بشكل جيد، والتخطي بسرعة إذا لم تعمل بشكل جيد.
كتابة التعليمات البرمجية في Java ليست صعبة وبناء الجملة بسيط. ونحن نعرف بالفعل المكتبات وواجهات برمجة التطبيقات الموجودة جيدًا. الأمر الصعب حقًا هو استخدامه لتطوير وصيانة التطبيقات على مستوى المؤسسة.
نحتاج إلى التأكد من أن الزملاء يغلقون اتصال قاعدة البيانات في الوقت الصحيح، وأنهم لا يستمرون في شغل المعاملات، وأن الاستثناءات يتم التعامل معها بشكل صحيح في الطبقة المناسبة، وأن الأقفال يتم الحصول عليها وتحريرها بشكل صحيح، وما إلى ذلك.
إذا أخذناها بشكل فردي، فإن أيًا من هذه القضايا لا يمثل مشكلة كبيرة. ومع ذلك، عندما تقترن بتعقيد المجال، تصبح المشكلة صعبة للغاية، وموارد التطوير محدودة، والصيانة صعبة.
ماذا سيحدث إذا قمنا بتغليف هذه الاستراتيجيات في العديد من الأجزاء الصغيرة من التعليمات البرمجية وتركناهم يقومون بإدارة القيود بشكل مستقل؟ إذن لن نضطر إلى إنفاق الطاقة باستمرار لتنفيذ الاستراتيجيات. هذا تحسن كبير، دعونا نلقي نظرة على كيفية قيام البرمجة الوظيفية بذلك.
تكرارات مجنونة
لقد قمنا بكتابة تكرارات مختلفة لمعالجة القوائم والمجموعات والخرائط. يعد استخدام التكرارات في Java أمرًا شائعًا جدًا، ولكنه معقد للغاية. فهي لا تشغل عدة أسطر من التعليمات البرمجية فحسب، بل يصعب تغليفها أيضًا.
كيف يمكننا تكرار المجموعة وطباعتها؟ يمكنك استخدام حلقة for. كيف نقوم بتصفية بعض العناصر من المجموعة؟ لا تزال تستخدم حلقة for، لكنك تحتاج إلى إضافة بعض المتغيرات الإضافية القابلة للتعديل. بعد تحديد هذه القيم، كيفية استخدامها للعثور على القيمة النهائية، مثل القيمة الدنيا، والحد الأقصى للقيمة، والقيمة المتوسطة، وما إلى ذلك؟ ثم عليك إعادة تدوير المتغيرات وتعديلها.
هذا النوع من التكرار يشبه الدواء الشافي، فهو يمكنه فعل كل شيء، ولكن كل شيء متناثر. توفر Java الآن تكرارات مضمنة للعديد من العمليات: على سبيل المثال، تلك التي تقوم بالحلقات فقط، وتلك التي تقوم بعمليات التعيين، وتلك التي تقوم بتصفية القيم، وتلك التي تقوم بتقليل العمليات، وهناك العديد من الوظائف الملائمة مثل الحد الأقصى والحد الأدنى و متوسط الخ. بالإضافة إلى ذلك، يمكن دمج هذه العمليات جيدًا، حتى نتمكن من تجميعها معًا لتنفيذ منطق الأعمال، وهو أمر بسيط ويتطلب تعليمات برمجية أقل. علاوة على ذلك، فإن الكود المكتوب سهل القراءة للغاية لأنه يتوافق منطقيًا مع ترتيب وصف المشكلة. وسنرى عدة أمثلة من هذا القبيل في الفصل الثاني، استخدام المجموعات، في الصفحة 19، وهذا الكتاب مليء بهذه الأمثلة.
تطبيق الإستراتيجية
يتم تنفيذ السياسات عبر التطبيقات على مستوى المؤسسة. على سبيل المثال، نحتاج إلى التأكد من مصادقة العملية بشكل صحيح للأمان، ونحتاج إلى التأكد من إمكانية تنفيذ المعاملة بسرعة، وتحديث سجل التعديل بشكل صحيح. عادةً ما تنتهي هذه المهام بكونها جزءًا من التعليمات البرمجية العادية على جانب الخادم، على غرار الكود الزائف التالي:
انسخ رمز الكود كما يلي:
معاملة المعاملة = getFromTransactionFactory();
//... العملية التي سيتم تنفيذها داخل المعاملة ...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
هناك مشكلتان في هذا النهج. أولا، غالبا ما يؤدي ذلك إلى ازدواجية الجهود ويزيد أيضا من تكاليف الصيانة. ثانيًا، من السهل نسيان الاستثناءات التي قد يتم طرحها في كود العمل، والتي قد تؤثر على دورة حياة المعاملة وتحديث سجل التعديل. يجب تنفيذ ذلك باستخدام المحاولة والأخيرة، ولكن في كل مرة يلمس شخص ما هذا الرمز، علينا إعادة التأكيد على أن هذه الإستراتيجية لم يتم تدميرها.
وهناك طريقة أخرى يمكننا من خلالها إزالة المصنع ووضع هذا الكود أمامه. بدلاً من الحصول على كائن المعاملة، قم بتمرير التعليمات البرمجية المنفذة إلى وظيفة جيدة الصيانة، مثل هذا:
انسخ رمز الكود كما يلي:
runWithinTransaction((معاملة المعاملة) -> {
//... العملية التي سيتم تنفيذها داخل المعاملة ...
});
إنها خطوة صغيرة بالنسبة لك، لكنها توفر عليك الكثير من المتاعب. يتم تلخيص استراتيجية التحقق من الحالة وتحديث السجل في نفس الوقت وتغليفها في طريقة runWithinTransaction. نرسل إلى هذه الطريقة جزءًا من التعليمات البرمجية التي يجب تشغيلها في سياق المعاملة. لم يعد هناك ما يدعو للقلق بشأن نسيان شخص ما تنفيذ هذه الخطوة أو عدم التعامل مع الاستثناء بشكل صحيح. الوظيفة التي تنفذ السياسة تعتني بذلك بالفعل.
سنغطي كيفية استخدام تعبيرات لامدا لتطبيق هذه الإستراتيجية في الفصل الخامس.
استراتيجية التوسع
يبدو أن الاستراتيجيات موجودة في كل مكان. بالإضافة إلى تطبيقها، تحتاج تطبيقات المؤسسات أيضًا إلى توسيعها. نأمل أن نضيف أو نحذف بعض العمليات من خلال بعض معلومات التكوين، بمعنى آخر، يمكننا معالجتها قبل تنفيذ المنطق الأساسي للوحدة. هذا أمر شائع جدًا في Java، ولكن يجب التفكير فيه وتصميمه مسبقًا.
عادةً ما تحتوي المكونات التي تحتاج إلى توسيع على واجهة واحدة أو أكثر. نحتاج إلى تصميم الواجهة والبنية الهرمية لفئات التنفيذ بعناية. قد يعمل هذا بشكل جيد، لكنه سيترك لك مجموعة من الواجهات والفئات التي تحتاج إلى الصيانة. يمكن أن يصبح مثل هذا التصميم غير عملي بسهولة ويصعب صيانته، مما يؤدي في النهاية إلى إحباط غرض القياس في المقام الأول.
هناك حل آخر - الواجهات الوظيفية وتعبيرات لامدا، والتي يمكننا استخدامها لتصميم استراتيجيات قابلة للتطوير. لا يتعين علينا إنشاء واجهة جديدة أو اتباع نفس اسم الطريقة، بل يمكننا التركيز أكثر على منطق الأعمال المراد تنفيذه، وهو ما سنذكره عند استخدام تعبيرات lambda للزخرفة في الصفحة 73.
أصبح التزامن سهلا
يقترب تطبيق كبير من مرحلة الإصدار عندما تظهر فجأة مشكلة خطيرة في الأداء. وسرعان ما قرر الفريق أن عنق الزجاجة في الأداء كان في وحدة ضخمة تعالج كميات هائلة من البيانات. اقترح أحد أعضاء الفريق إمكانية تحسين أداء النظام إذا أمكن استغلال مزايا النواة المتعددة بشكل كامل. ومع ذلك، إذا تمت كتابة هذه الوحدة الضخمة بأسلوب Java القديم، فسوف تتبدد الفرحة التي جلبها هذا الاقتراح قريبًا.
وسرعان ما أدرك الفريق أن تغيير هذا العملاق من التنفيذ التسلسلي إلى التنفيذ المتوازي سيتطلب الكثير من الجهد، ويضيف تعقيدًا إضافيًا، ويتسبب بسهولة في حدوث أخطاء متعلقة بسلاسل العمليات المتعددة. أليس هناك طريقة أفضل لتحسين الأداء؟
هل من الممكن أن يكون الكود التسلسلي والمتوازي متماثلين، بغض النظر عما إذا اخترت التنفيذ التسلسلي أو المتوازي، تمامًا مثل الضغط على المفتاح والتعبير عن فكرتك؟
يبدو أن هذا ممكن فقط في نارنيا، لكن إذا تطورنا بشكل كامل من الناحية الوظيفية، كل هذا سيصبح حقيقة. ستعمل التكرارات المضمنة والأسلوب الوظيفي على إزالة العقبة الأخيرة أمام الموازاة. يسمح تصميم JDK بالتبديل بين التنفيذ التسلسلي والمتوازي مع بعض التغييرات غير الواضحة في التعليمات البرمجية، والتي سنذكرها في "إكمال القفزة إلى الموازاة" في الصفحة 145.
أخبر القصص
يتم فقدان الكثير من الأشياء أثناء عملية تحويل متطلبات العمل إلى تنفيذ التعليمات البرمجية. وكلما زادت الخسارة، زاد احتمال الخطأ وتكلفة الإدارة. إذا كان الكود يبدو وكأنه يصف المتطلبات، فسيكون من الأسهل قراءته، وسيكون من الأسهل مناقشته مع الأشخاص ذوي المتطلبات، وسيكون من الأسهل تلبية احتياجاتهم.
على سبيل المثال، تسمع مدير المنتج يقول: "احصل على أسعار جميع الأسهم، وابحث عن تلك التي تزيد أسعارها عن 500 يوان، واحسب إجمالي الأصول التي يمكن أن تدفع أرباحًا." باستخدام المرافق الجديدة التي توفرها Java، يمكنك الكتابة:
انسخ رمز الكود كما يلي:
Tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
عملية التحويل هذه تكاد تكون بلا خسارة لأنه لا يوجد أي شيء يمكن تحويله. هذا هو الأسلوب الوظيفي في العمل، وسترى العديد من الأمثلة على ذلك في جميع أنحاء الكتاب، وخاصة الفصل الثامن، بناء البرامج باستخدام تعبيرات Lambda، الصفحة 137.
التركيز على الحجر الصحي
في تطوير النظام، عادةً ما يلزم عزل الأعمال الأساسية والمنطق الدقيق الذي يتطلبه. على سبيل المثال، قد يرغب نظام معالجة الطلب في استخدام استراتيجيات ضريبية مختلفة لمصادر المعاملات المختلفة. يؤدي عزل حسابات الضرائب عن بقية منطق المعالجة إلى جعل التعليمات البرمجية أكثر قابلية لإعادة الاستخدام وقابلة للتطوير.
في البرمجة الموجهة للكائنات، نسمي هذا الاهتمام بالعزلة، وعادة ما يستخدم نمط الإستراتيجية لحل هذه المشكلة. الحل بشكل عام هو إنشاء بعض الواجهات وفئات التنفيذ.
يمكننا تحقيق نفس التأثير باستخدام كود أقل. يمكننا أيضًا تجربة أفكار المنتجات الخاصة بنا بسرعة دون الحاجة إلى التوصل إلى مجموعة من الأكواد والركود. سنستكشف المزيد عن كيفية إنشاء هذا النمط وإجراء عزل الاهتمامات من خلال وظائف خفيفة الوزن في عزل الاهتمامات باستخدام تعبيرات Lambda في الصفحة 63.
تقييم كسول
عند تطوير التطبيقات على مستوى المؤسسة، قد نتفاعل مع خدمات الويب، وقواعد بيانات الاتصال، ومعالجة XML، وما إلى ذلك. هناك العديد من العمليات التي يتعين علينا القيام بها، ولكن ليست جميعها مطلوبة طوال الوقت. يعد تجنب عمليات معينة أو على الأقل تأخير بعض العمليات غير الضرورية مؤقتًا أحد أسهل الطرق لتحسين الأداء أو تقليل وقت بدء تشغيل البرنامج ووقت الاستجابة.
هذا مجرد شيء صغير، لكنه يتطلب الكثير من العمل لتنفيذه بطريقة OOP خالصة. من أجل تأخير تهيئة بعض الكائنات ذات الوزن الثقيل، يتعين علينا التعامل مع مراجع الكائنات المختلفة، والتحقق من وجود مؤشرات فارغة، وما إلى ذلك.
ومع ذلك، إذا كنت تستخدم فئة Optinal الجديدة وبعض واجهات برمجة التطبيقات ذات الأنماط الوظيفية التي توفرها، فستصبح هذه العملية بسيطة للغاية وسيكون الكود أكثر وضوحًا، وسنناقش هذا في التهيئة البطيئة في الصفحة 105.
تحسين قابلية الاختبار
كلما كان منطق المعالجة أقل في الكود، قل احتمال تصحيح الأخطاء. بشكل عام، الكود الوظيفي أسهل في التعديل والاختبار.
بالإضافة إلى ذلك، تمامًا مثل الفصل 4، التصميم باستخدام تعبيرات Lambda والفصل 5، استخدام الموارد، يمكن استخدام تعبيرات lambda ككائن وهمي خفيف الوزن لجعل اختبار الاستثناء أكثر وضوحًا وأسهل للفهم. يمكن أيضًا أن تكون تعبيرات Lambda بمثابة أداة مساعدة رائعة في الاختبار. يمكن للعديد من حالات الاختبار الشائعة قبول تعبيرات لامدا والتعامل معها. يمكن لحالات الاختبار المكتوبة بهذه الطريقة التقاط جوهر الوظيفة التي تحتاج إلى اختبار الانحدار. وفي الوقت نفسه، يمكن إكمال العديد من التطبيقات التي تحتاج إلى الاختبار عن طريق تمرير تعبيرات لامدا المختلفة.
تُعد حالات الاختبار الآلي الخاصة بـ JDK أيضًا مثالًا جيدًا لتطبيق تعبيرات lambda - إذا كنت تريد معرفة المزيد، فيمكنك إلقاء نظرة على الكود المصدري في مستودع OpenJDK. من خلال برامج الاختبار هذه، يمكنك معرفة كيفية تحديد تعبيرات لامدا للسلوكيات الرئيسية لحالة الاختبار؛ على سبيل المثال، يقومون ببناء برنامج اختبار مثل هذا، "إنشاء حاوية للنتائج"، ثم "إضافة بعض الشروط اللاحقة ذات المعلمات".
لقد رأينا أن البرمجة الوظيفية لا تسمح لنا بكتابة تعليمات برمجية عالية الجودة فحسب، بل إنها تحل أيضًا المشكلات المختلفة أثناء عملية التطوير بشكل أنيق. وهذا يعني أن تطوير البرامج سيصبح أسرع وأسهل، مع أخطاء أقل - طالما أنك تتبع بعض الإرشادات التي سنقدمها لاحقًا.
القسم 4: التطور وليس الثورة
لا نحتاج إلى التبديل إلى لغة أخرى للاستمتاع بفوائد البرمجة الوظيفية؛ كل ما نحتاج إلى تغييره هو الطريقة التي نستخدم بها Java. تدعم لغات مثل C++ وJava وC# البرمجة الحتمية والموجهة للكائنات. لكنهم الآن بدأوا في تبني البرمجة الوظيفية. لقد نظرنا للتو إلى كلا النمطين من التعليمات البرمجية وناقشنا الفوائد التي يمكن أن تجلبها البرمجة الوظيفية. الآن دعونا نلقي نظرة على بعض المفاهيم والأمثلة الأساسية لمساعدتنا في تعلم هذا الأسلوب الجديد.
لقد أمضى فريق تطوير لغة Java الكثير من الوقت والطاقة في إضافة إمكانات البرمجة الوظيفية إلى لغة Java وJDK. للاستمتاع بالفوائد التي تجلبها، علينا تقديم بعض المفاهيم الجديدة أولاً. يمكننا تحسين جودة الكود الخاص بنا طالما أننا نتبع القواعد التالية:
1. تصريحي
2. تعزيز الثبات
3. تجنب الآثار الجانبية
4. تفضيل التعبيرات على البيانات
5. التصميم باستخدام وظائف ذات ترتيب أعلى
دعونا نلقي نظرة على هذه المبادئ التوجيهية العملية.
تصريحي
إن جوهر ما نعرفه كبرمجة حتمية هو التباين والبرمجة المبنية على الأوامر. نقوم بإنشاء المتغيرات ثم نقوم بتعديل قيمها بشكل مستمر. كما نقدم أيضًا تعليمات مفصلة ليتم تنفيذها، مثل إنشاء علامة فهرس للتكرار، وزيادة قيمته، والتحقق من انتهاء الحلقة، وتحديث العنصر N للمصفوفة، وما إلى ذلك. في الماضي، نظرًا لخصائص الأدوات وقيود الأجهزة، لم نتمكن من كتابة التعليمات البرمجية إلا بهذه الطريقة. لقد رأينا أيضًا أنه في المجموعة غير القابلة للتغيير، فإن الطريقة التي تحتوي على التصريح أسهل في الاستخدام من الطريقة الحتمية. يتم تنفيذ جميع المشكلات الصعبة والعمليات منخفضة المستوى في وظائف المكتبة، ولا داعي للقلق بشأن هذه التفاصيل بعد الآن. من أجل التبسيط، يجب علينا أيضًا استخدام البرمجة التعريفية. إن الثبات والبرمجة التعريفية هما جوهر البرمجة الوظيفية، والآن جعلتها Java حقيقة واقعة.
تعزيز الثبات
سيكون للكود ذو المتغيرات القابلة للتغيير العديد من مسارات النشاط. كلما قمت بتغيير المزيد من الأشياء، أصبح من الأسهل تدمير البنية الأصلية وإدخال المزيد من الأخطاء. من الصعب فهم التعليمات البرمجية التي تحتوي على متغيرات متعددة ويصعب موازنتها. الثبات يزيل هذه المخاوف بشكل أساسي. تدعم Java الثبات ولكنها لا تتطلب ذلك - ولكننا نستطيع ذلك. نحن بحاجة إلى تغيير العادة القديمة المتمثلة في تعديل حالة الكائن. يجب علينا استخدام الكائنات غير القابلة للتغيير قدر الإمكان. عند الإعلان عن المتغيرات والأعضاء والمعلمات، حاول إعلانها نهائية، تمامًا مثل مقولة جوشوا بلوخ الشهيرة في "Java الفعالة"، "تعامل مع الكائنات على أنها غير قابلة للتغيير". عند إنشاء كائنات، حاول إنشاء كائنات غير قابلة للتغيير، مثل String. عند إنشاء مجموعة، حاول إنشاء مجموعة غير قابلة للتغيير أو التعديل، مثل استخدام طرق مثل Arrays.asList() وUnmodifiableList() للمجموعات. من خلال تجنب التباين، يمكننا كتابة وظائف خالصة - أي وظائف بدون آثار جانبية.
تجنب الآثار الجانبية
لنفترض أنك تكتب جزءًا من التعليمات البرمجية للحصول على سعر السهم من الإنترنت وكتابته في متغير مشترك. إذا كان لدينا العديد من الأسعار لجلبها، فيجب علينا تنفيذ هذه العمليات التي تستغرق وقتًا طويلاً بشكل تسلسلي. إذا أردنا الاستفادة من قوة تعدد العمليات، فعلينا أن نتعامل مع متاعب الترابط والمزامنة لمنع حالات السباق. والنتيجة النهائية أن أداء البرنامج ضعيف جداً، وينسى الناس الأكل والنوم من أجل الحفاظ على الخيط. إذا تم القضاء على الآثار الجانبية، يمكننا تجنب هذه المشاكل تماما. تعمل الوظيفة التي ليس لها آثار جانبية على تعزيز الثبات ولا تقوم بتعديل أي مدخلات أو أي شيء آخر ضمن نطاقها. هذا النوع من الوظائف سهل القراءة للغاية، ويحتوي على أخطاء قليلة، وسهل التحسين. نظرًا لعدم وجود أي آثار جانبية، فلا داعي للقلق بشأن ظروف السباق أو التعديلات المتزامنة. ليس هذا فحسب، بل يمكننا بسهولة تنفيذ هذه الوظائف بالتوازي، وهو ما سنناقشه في الصفحة 145.
تفضل التعبيرات
التصريحات هي البطاطا الساخنة لأنها تفرض التعديل. تعزز التعبيرات الثبات وتكوين الوظيفة. على سبيل المثال، نستخدم أولاً عبارة for لحساب السعر الإجمالي بعد الخصومات. يؤدي هذا الكود إلى التباين والكود المطول. إن استخدام إصدارات أكثر تعبيرًا وتعريفًا للخريطة وطرق الجمع لا يؤدي إلى تجنب عمليات التعديل فحسب، بل يسمح أيضًا بربط الوظائف معًا. عند كتابة التعليمات البرمجية، يجب أن تحاول استخدام التعبيرات بدلاً من البيانات. وهذا يجعل الكود أبسط وأسهل للفهم. سيتم تنفيذ التعليمات البرمجية وفقًا لمنطق العمل، تمامًا كما وصفنا المشكلة. من المؤكد أن النسخة المختصرة أسهل في التعديل إذا تغيرت المتطلبات.
التصميم باستخدام وظائف ذات ترتيب أعلى
لا تفرض Java الثبات مثل اللغات الوظيفية مثل Haskell، ولكنها تسمح لنا بتعديل المتغيرات. لذلك، فإن Java ليست ولن تكون أبدًا لغة برمجة وظيفية بحتة. ومع ذلك، يمكننا استخدام وظائف ذات ترتيب أعلى للبرمجة الوظيفية في جافا. تنقل الوظائف ذات الترتيب الأعلى عملية إعادة الاستخدام إلى المستوى التالي. باستخدام الوظائف عالية الترتيب، يمكننا بسهولة إعادة استخدام التعليمات البرمجية الناضجة التي تكون صغيرة ومتخصصة ومتماسكة للغاية. في OOP، اعتدنا على تمرير الكائنات إلى الأساليب وإنشاء كائنات جديدة في الأساليب ثم إرجاع الكائنات. تقوم الوظائف ذات الترتيب الأعلى بنفس الأشياء التي تقوم بها الأساليب للكائنات. مع وظائف النظام الأعلى نستطيع.
1. قم بتمرير الوظيفة إلى الوظيفة
2. قم بإنشاء وظيفة جديدة داخل الوظيفة
3. إرجاع الوظائف داخل الوظائف
لقد رأينا بالفعل مثالاً على تمرير المعلمات من دالة إلى دالة أخرى، وسنرى لاحقًا أمثلة على إنشاء الوظائف وإرجاعها. لنلقِ نظرة على مثال "تمرير المعلمات إلى دالة" مرة أخرى:
انسخ رمز الكود كما يلي:
الأسعار. تيار ()
.filter(price ->price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price ->price.multiply(BigDecimal.valueOf(0.9)))
تقرير خطأ • مناقشة
.reduce(BigDecimal.ZERO, BigDecimal::add);
في هذا الكود، نقوم بتمرير سعر الدالة -> Price.multiply(BigDecimal.valueOf(0.9)) إلى دالة الخريطة. يتم إنشاء الوظيفة التي تم تمريرها عند استدعاء خريطة الوظيفة ذات الترتيب الأعلى. بشكل عام، تحتوي الوظيفة على نص الوظيفة واسم الوظيفة وقائمة المعلمات وقيمة الإرجاع. تحتوي هذه الوظيفة التي تم إنشاؤها بسرعة على قائمة معلمات متبوعة بسهم (->)، ثم نص دالة قصير. يتم استنتاج أنواع المعلمات بواسطة مترجم Java، ونوع الإرجاع ضمني أيضًا. هذه وظيفة مجهولة، وليس لها اسم. لكننا لا نسميها دالة مجهولة، بل نسميها تعبير لامدا. إن تمرير الدوال المجهولة كمعلمات ليس بالأمر الجديد في Java؛ لقد مررنا في كثير من الأحيان فئات داخلية مجهولة من قبل. حتى لو كان للفصل المجهول طريقة واحدة فقط، فلا يزال يتعين علينا اتباع طقوس إنشاء الفصل وإنشاء مثيل له. مع تعبيرات لامدا يمكننا الاستمتاع ببناء جملة خفيف الوزن. ليس هذا فحسب، فقد اعتدنا دائمًا على تلخيص بعض المفاهيم في كائنات مختلفة، ولكن الآن يمكننا تجريد بعض السلوكيات في تعبيرات لامدا. لا تزال البرمجة باستخدام أسلوب الترميز هذا تتطلب بعض التفكير. علينا أن نحول تفكيرنا الحتمي المتأصل بالفعل إلى تفكير وظيفي. قد يكون الأمر مؤلمًا بعض الشيء في البداية، ولكنك ستعتاد عليه قريبًا مع استمرارك في التعمق، سيتم تجاهل واجهات برمجة التطبيقات غير الوظيفية هذه تدريجيًا. دعونا نتوقف عن هذا الموضوع أولاً، دعونا نلقي نظرة على كيفية تعامل جافا مع تعبيرات لامدا. اعتدنا دائمًا تمرير الكائنات إلى الأساليب، والآن يمكننا تخزين الوظائف وتمريرها. دعونا نلقي نظرة على السر وراء قدرة Java على أخذ الوظائف كمعلمات.
القسم 5: إضافة بعض السكر النحوي
يمكن تحقيق ذلك أيضًا باستخدام وظائف Java الأصلية، لكن تعبيرات لامدا تضيف بعض السكر النحوي، مما يوفر بعض الخطوات ويجعل عملنا أسهل. الكود المكتوب بهذه الطريقة لا يتطور بشكل أسرع فحسب، بل يعبر أيضًا عن أفكارنا بشكل أفضل. العديد من الواجهات التي استخدمناها في الماضي كانت تحتوي على طريقة واحدة فقط: قابلة للتشغيل، وقابلة للاستدعاء، وما إلى ذلك. يمكن العثور على هذه الواجهات في كل مكان في مكتبة JDK، وحيثما يتم استخدامها، يمكن عادةً إجراؤها باستخدام إحدى الوظائف. يمكن الآن لوظائف المكتبة التي كانت تتطلب في السابق واجهة أحادية الأسلوب فقط تمرير وظائف خفيفة الوزن، وذلك بفضل السكر النحوي الذي توفره الواجهات الوظيفية. الواجهة الوظيفية هي واجهة ذات طريقة مجردة واحدة فقط. انظر إلى تلك الواجهات التي تحتوي على طريقة واحدة فقط، Runnable، Callable، وما إلى ذلك، وهذا التعريف ينطبق عليها. هناك المزيد من هذه الواجهات في JDK8 - الوظيفة، والمسند، والمستهلك، والمورد، وما إلى ذلك (صفحة 157، يحتوي الملحق 1 على قائمة واجهات أكثر تفصيلاً). يمكن أن تحتوي الواجهات الوظيفية على العديد من الأساليب الثابتة والأساليب الافتراضية التي يتم تنفيذها في الواجهة. يمكننا استخدام التعليق التوضيحي @FunctionalInterface للتعليق على الواجهة الوظيفية. لا يستخدم المترجم هذا التعليق التوضيحي، ولكن يمكنه تحديد نوع هذه الواجهة بشكل أكثر وضوحًا. ليس ذلك فحسب ، إذا قمنا بتوضيح واجهة مع هذا التعليق التوضيحي ، فسيتحقق المترجم بقوة ما إذا كان يتوافق مع قواعد الواجهات الوظيفية. إذا تلقى طريقة ما واجهة وظيفية كمعلمة ، فإن المعلمات التي يمكننا تمريرها تتضمن:
1. فصول داخلية مجهولة ، أقدم طريقة
2. تعبير Lambda ، تمامًا كما فعلنا في طريقة الخريطة
3. الإشارة إلى طريقة أو مُنشئ (سنتحدث عنها لاحقًا)
إذا كانت معلمة الطريقة عبارة عن واجهة وظيفية ، فسيقبل المترجم بسعادة تعبير Lambda أو مرجع الطريقة كمعلمة. إذا تم نقل تعبير Lambda إلى طريقة ، فسيقوم برنامج التحويل البرمجي أولاً بتحويل التعبير إلى مثيل للواجهة الوظيفية المقابلة. هذا التحول هو أكثر من مجرد توليد فئة داخلية. تتوافق طرق هذا المثال الذي تم إنشاؤه بشكل متزامن مع الطرق المجردة للواجهة الوظيفية للمعلمة. على سبيل المثال ، تتلقى طريقة الخريطة وظيفة الواجهة الوظيفية كمعلمة. عند استدعاء طريقة الخريطة ، سيقوم برنامج التحويل البرمجي Java بإنشائه بشكل متزامن ، كما هو موضح في الشكل أدناه.
يجب أن تتطابق معلمات تعبير Lambda مع معلمات الطريقة التجريدية للواجهة. هذه الطريقة التي تم إنشاؤها ستعيد نتيجة تعبير Lambda. إذا كان نوع الإرجاع لا يتطابق مباشرة مع الطريقة التجريدية ، فستقوم هذه الطريقة بتحويل قيمة الإرجاع إلى النوع المناسب. لقد تلقينا بالفعل نظرة عامة على كيفية نقل تعبيرات Lambda إلى الأساليب. دعنا نراجع بسرعة ما تحدثنا عنه للتو ، ثم نبدأ استكشافنا لتعبيرات Lambda.
تلخيص
هذه منطقة جديدة تمامًا من جافا. من خلال الوظائف ذات الترتيب العالي ، يمكننا الآن كتابة رمز نمط وظيفي أنيق وطلاقة. الكود المكتوب بهذه الطريقة موجز وسهل الفهم ، وله أخطاء قليلة ، ويؤدي إلى الصيانة والموازاة. يعمل برنامج التحويل البرمجي Java سحره ، وحيث نتلقى معلمات الواجهة الوظيفية ، يمكننا تمرير تعبيرات Lambda أو مراجع الطريقة. يمكننا الآن الدخول إلى عالم تعبيرات Lambda وتكييف مكتبات JDK ليشعروا بالمرح منها. في الفصل التالي ، سنبدأ مع العمليات الأكثر شيوعًا في البرمجة وإطلاق العنان لقوة تعبيرات Lambda.