কম্পোজিশনের দুই ধরনের গঠন

এখন পর্যন্ত দেখা প্রতিটি পাইপলাইনই সোজা: একটি অ্যাসিঙ্ক ধাপ thenApply-এর মাধ্যমে পরবর্তী ধাপে মান পাঠায়। বাস্তব প্রোগ্রামে আরও দুটি গঠন প্রায় সমানভাবেই দরকার হয়। কখনও দ্বিতীয় ধাপটি প্রথম ধাপের ফলাফলের ওপর নির্ভর করে এবং নিজেও অ্যাসিঙ্ক্রোনাস হয় — যেমন কোনো ইউজার খোঁজা, তারপর সেই ইউজারের আইডি দিয়ে তার অর্ডার খোঁজা। আবার কখনও দুটি ধাপ সম্পূর্ণ স্বাধীন এবং তুমি চাও দুটির ফলাফলই প্রস্তুত হওয়ার পর একসাথে পাও — যেমন একই সঙ্গে ইউজার প্রোফাইল ও অ্যাকাউন্ট ব্যালেন্স ফেচ করে একটি রেসপন্সে জুড়ে দেওয়া। CompletableFuture-এ প্রতিটি গঠনের জন্য আলাদা মেথড আছে, আর ভুল মেথড বেছে নেওয়া বিভ্রান্তির একটি সাধারণ উৎস।

নির্ভরশীল অ্যাসিঙ্ক ধাপ চেইন করা: thenCompose

যদি তুমি thenApply ব্যবহার করো অথচ পরবর্তী ধাপ নিজেই একটি CompletableFuture রিটার্ন করে, তাহলে তুমি সমতল পাইপলাইন পাও না — পাও একটি CompletableFuture<CompletableFuture<T>>, অর্থাৎ ফিউচারের ভেতরে আরেকটি ফিউচার, যা সাধারণত কাম্য নয়।

// ভুল: তৈরি হয় CompletableFuture<CompletableFuture<List<Order>>>
CompletableFuture<CompletableFuture<List<Order>>> nested = CompletableFuture
    .supplyAsync(() -> lookupUser(id))
    .thenApply(user -> lookupOrdersAsync(user.getId()));

উপরের কোডে lookupOrdersAsync নিজেই একটি CompletableFuture রিটার্ন করে বলে thenApply সেটিকে আরেকটি ফিউচারের ভেতরে মুড়ে ফেলে। ফলে একটি নেস্টেড স্ট্রাকচার তৈরি হয়, যা নিয়ে পরে কাজ করা অসুবিধাজনক।

thenCompose এই নেস্টিং সমতল করে দেয়। এটি আশা করে কলব্যাকটি একটি CompletableFuture রিটার্ন করবে, এবং সেটিকে স্বয়ংক্রিয়ভাবে আনর‍্যাপ করে একটি একক, সমতল ফিউচারে পরিণত করে:

CompletableFuture<List<Order>> orders = CompletableFuture
    .supplyAsync(() -> lookupUser(id))
    .thenCompose(user -> lookupOrdersAsync(user.getId()));

এখানে thenCompose নিশ্চিত করে যে শেষ ফলাফল CompletableFuture<List<Order>> টাইপের — কোনো নেস্টেড ফিউচার নয়। সহজ নিয়ম: কলব্যাক যদি সাধারণ মান রিটার্ন করে তবে thenApply, আর কলব্যাক নিজেই যদি CompletableFuture রিটার্ন করে তবে thenCompose ব্যবহার করো। একই পার্থক্য Optional.mapOptional.flatMap-এর মধ্যেও দেখা যায়, একই কারণে।

দুটি স্বাধীন ফিউচার মার্জ করা: thenCombine

thenCompose নির্ভরশীল ধাপের জন্য — দ্বিতীয় ধাপ শুরু হওয়ার আগেই প্রথমটির ফলাফল দরকার হয়। যখন দুটি ধাপ স্বাধীন এবং একসঙ্গে চালানো সম্ভব, তখন thenCombine ব্যবহার করে দুটিকেই একবারে শুরু করা যায় এবং দুটির ফলাফল প্রস্তুত হলে তা মার্জ করা যায়।

CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> lookupUser(id));
CompletableFuture<Balance> balanceFuture = CompletableFuture.supplyAsync(() -> lookupBalance(id));

CompletableFuture<AccountSummary> summaryFuture = userFuture.thenCombine(
    balanceFuture,
    (user, balance) -> new AccountSummary(user, balance)
);

উপরের উদাহরণে lookupUser এবং lookupBalance — দুটিই supplyAsync কল করার মুহূর্তেই চালু হয়ে যায়। তারা একে অপরের জন্য অপেক্ষা না করে সমান্তরালে (concurrently) চলতে থাকে। মার্জ করার ফাংশনটি কেবল তখনই চলে যখন দুটি ফিউচারের কাজই শেষ হয়। এর বিপরীতে userFuture.get()balanceFuture.get() পরপর কল করলে দেখতে একই রকম লাগলেও, তা কলিং থ্রেডকে দুবার ব্লক করে। thenCombine একই কনকারেন্সি অর্জন করে পাইপলাইনে কোনো ব্লকিং কল ছাড়াই।

সঠিক টুল বাছাই করা

সিদ্ধান্ত নেওয়ার একটি কার্যকর উপায়: নিজেকে জিজ্ঞেস করো, দ্বিতীয় ধাপ কি প্রথম ধাপের আউটপুট ছাড়া কী করতে হবে সেটাই জানে না? যদি উত্তর হ্যাঁ হয়, তাহলে সেটি নির্ভরশীল সম্পর্ক — thenCompose ব্যবহার করো। যদি দুটি ধাপ একে অপরের ফলাফল ছাড়াই শুরু করা যায় এবং তুমি কেবল দুটির সম্মিলিত ফলাফল চাও, তাহলে সেটি স্বাধীন সম্পর্ক — thenCombine ব্যবহার করো। এই সহজ প্রশ্নটি ভুল মেথড বেছে নেওয়ার সম্ভাবনা প্রায় শূন্যে নামিয়ে আনে।

Share