تعابير لامدا والواجهات الوظيفيّة

تأليف السلوك باستخدام تعبيرات Lambda

15 دقيقة الدرس 9 من 13

تأليف السلوك باستخدام تعبيرات Lambda

تمرير تعبير lambda إلى دالة يعني أنك تُمرّر سلوكًا — وحدة منطق صغيرة مجهولة الاسم — بدلًا من مجرّد قيمة. الدوال التي تقبل دوالًا أخرى أو تُعيدها تُسمّى دوالًا من رتبة عليا (Higher-Order Methods). يتيح لك هذان المبدآن معًا كتابة كود يكون للأغراض العامة لكنه دقيق التعبير في كل موضع استدعاء.

لماذا نُمرّر السلوك أصلًا؟

فكّر في تصفية قائمة. يمكنك كتابة filterByAge وfilterByName وfilterByCity — دالة لكل قاعدة. أو يمكنك كتابة دالة filter واحدة تقبل القاعدة كـ Predicate وتُطبّقها على أي قائمة تمنحها إياها. المقاربة الثانية أقصر وقابلة لإعادة الاستخدام ومفتوحة لقواعد لم تخترعها بعد.

تعريف الدالة من رتبة عليا: الدالة تكون من رتبة عليا حين تقبل دالة كمعامل، أو تُعيد دالة، أو كليهما. كل دالة في Stream API — filter وmap وsorted — هي دالة من رتبة عليا.

كتابة أول دالة من رتبة عليا

إليك أداة مستقلة تُطبّق تحويلًا على كل عنصر في قائمة وتُعيد قائمة جديدة. يُعبَّر عن التحويل كـ Function<T, R> يُمرَّر من قبل المستدعي.

import java.util.ArrayList; import java.util.List; import java.util.function.Function; public class HigherOrderDemo { // دالة من رتبة عليا: تقبل السلوك كمعامل public static <T, R> List<R> transform(List<T> items, Function<T, R> mapper) { List<R> result = new ArrayList<>(); for (T item : items) { result.add(mapper.apply(item)); } return result; } public static void main(String[] args) { List<String> names = List.of("alice", "bob", "carol"); // تمرير سلوكيات مختلفة للدالة ذاتها List<String> upper = transform(names, String::toUpperCase); List<Integer> lengths = transform(names, String::length); System.out.println(upper); // [ALICE, BOB, CAROL] System.out.println(lengths); // [5, 3, 5] } }

أنتجت دالة transform ذاتها نتيجتين مختلفتين تمامًا لأن السلوك تغيّر لا الدالة. هذه هي القوة الجوهرية للبرمجة من رتبة عليا.

إعادة دالة من دالة

يمكن لدالة أيضًا أن تبني وتُعيد تعبير lambda. يُسمّى هذا مصنعًا للسلوك: تستدعي الدالة مع بعض الإعداد، وتُعيد لك دالة جاهزة للاستخدام.

import java.util.function.Predicate; public class BehaviourFactory { // تُعيد Predicate يتحقّق مما إذا كانت السلسلة تبدأ بالبادئة المحددة public static Predicate<String> startsWith(String prefix) { return s -> s.startsWith(prefix); } // تُعيد Predicate يتحقّق من الحد الأدنى للطول public static Predicate<String> longerThan(int minLength) { return s -> s.length() > minLength; } public static void main(String[] args) { Predicate<String> isJava = startsWith("Java"); Predicate<String> isLong = longerThan(5); System.out.println(isJava.test("JavaScript")); // true System.out.println(isJava.test("Python")); // false System.out.println(isLong.test("Hi")); // false System.out.println(isLong.test("Lambdas")); // true // تركيب باستخدام مُدمِجات Predicate — انظر الدرس الرابع Predicate<String> javaAndLong = isJava.and(isLong); System.out.println(javaAndLong.test("JavaScript")); // true System.out.println(javaAndLong.test("Java")); // false (الطول 4) } }
التقاط الإعداد لا الحالة. تعبير lambda الذي تُعيده startsWith("Java") يلتقط المتغير prefix. هذا المتغير يكون فعليًا نهائيًا بمجرد عودة الدالة — وتعبير lambda آمن للتخزين والمشاركة والاستدعاء لاحقًا من أي خيط. هذه هي الطريقة الصحيحة لتحديد معاملات السلوك.

تأليف سلسلة معالجة بتسلسل الاستدعاءات

يمكنك تمرير نتيجة استدعاء دالة من رتبة عليا مباشرةً إلى دالة أخرى، وبناء سلسلة معالجة تعريفية مقروءة دون متغيرات وسيطة:

import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; public class PipelineDemo { public static <T> List<T> filterAndCollect(List<T> source, Predicate<T> condition) { return source.stream() .filter(condition) .collect(Collectors.toList()); } public static void main(String[] args) { List<String> languages = List.of("Java", "JavaScript", "Python", "Kotlin", "Julia"); // بناء predicate مُركّب مباشرةً وتمريره للدالة List<String> result = filterAndCollect( languages, BehaviourFactory.startsWith("J").and(BehaviourFactory.longerThan(4)) ); System.out.println(result); // [JavaScript, Julia] } }

مثال عملي: تقرير قابل للفرز والتصفية

غالبًا ما تحتاج التطبيقات الحقيقية إلى فرز وتصفية بطرق غير معروفة وقت الترجمة. تمرير Comparator وPredicate كمعاملات يُبقي الدالة عامة.

import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; public record Product(String name, double price, String category) {} public class ReportEngine { public static List<Product> buildReport( List<Product> products, Predicate<Product> filter, Comparator<Product> sorter) { return products.stream() .filter(filter) .sorted(sorter) .collect(Collectors.toList()); } public static void main(String[] args) { List<Product> catalogue = List.of( new Product("Laptop", 999.0, "Electronics"), new Product("Phone", 499.0, "Electronics"), new Product("Desk", 250.0, "Furniture"), new Product("Monitor", 350.0, "Electronics") ); // المستدعي يحدد القاعدة والترتيب — الدالة لا تتغيّر أبدًا List<Product> report = buildReport( catalogue, p -> p.category().equals("Electronics") && p.price() < 600, Comparator.comparingDouble(Product::price) ); report.forEach(p -> System.out.printf("%s $%.2f%n", p.name(), p.price())); // Phone $499.00 // Monitor $350.00 } }
لا تُبالغ في استخدام هذا النمط. تتألّق الدوال من رتبة عليا حين تكون نقطة التغيير مفتوحة حقًا أو مُعاد استخدامها من مواضع استدعاء متعددة. إن كانت الدالة تُستدعى دائمًا بتعبير lambda واحد ثابت، فالدالة العادية أكثر وضوحًا. استخدم هذا النمط حين تُبرّر المرونةُ تعقيدَها.

تأليف الدوال مع andThen وcompose

تُوفّر واجهة Function مساعدَين مدمجَين للتأليف. f.andThen(g) ينشئ دالة جديدة تُطبّق f أولًا ثم تُغذّي النتيجة إلى g. أما f.compose(g) فيعمل بعكس ذلك — g أولًا ثم f.

import java.util.function.Function; public class FunctionComposition { public static void main(String[] args) { Function<String, String> trim = String::strip; Function<String, String> lower = String::toLowerCase; Function<String, Integer> length = String::length; // سلسلة المعالجة: trim ثم lower ثم length Function<String, Integer> pipeline = trim.andThen(lower).andThen(length); System.out.println(pipeline.apply(" Hello World ")); // 11 // إعادة دالة مُركّبة من دالة مساعدة System.out.println(normalize().apply(" JAVA ")); // "java" } // دالة من رتبة عليا: تُعيد Function مُركّبة public static Function<String, String> normalize() { return ((Function<String, String>) String::strip).andThen(String::toLowerCase); } }

الخلاصة

تُعامل الدوال من رتبة عليا تعبيرات lambda كوسيطات ذات مرتبة أولى وقيم يمكن إعادتها. يتيح لك ذلك كتابة خوارزمية عامة واحدة وتزويد السلوك المتغيّر عند موضع الاستدعاء. ادمج ذلك مع مساعدي التأليف — andThen وcompose وand وor وnegate — وستتمكن من بناء سلاسل سلوك غنية ومقروءة دون فئات إضافية أو منطق مُكرَّر.