প্রোপার্টি ও এর অ্যাট্রিবিউটের ব্যবহার

ভূমিকাঃ গত অধ্যায়ে অবজেকটিভ সি তে অবজেক্ট অরিয়েন্টেড প্রোগ্রামিং এর প্রয়োগ নিয়া আলোচনা হয়েছে। প্রোপার্টি, মেথড, ক্লাস, ইন্টারফেস, ইমপ্লিমেন্টেশন, অবজেক্ট ইন্সট্যান্সিয়েট ও সেগুলোর ব্যবহার সম্পর্কে ধারনা পেয়েছি আমরা। এই অধ্যায়ে প্রোপার্টি ও এর অ্যাট্রিবিউট গুলোর ব্যবহার নিয়ে বিস্তারিত আলোচনা হবে সাথে মেমোরি ম্যানেজমেন্ট নিয়েও প্রাসঙ্গিক আলোচনা চলে আসবে।

সাধারণত, একটি অবজেক্টের প্রোপার্টিগুলো অন্য আরেকটি অবজেক্টকে তাদের অবস্থা পরিবর্তন এবং যাচাই করতে দেয়। কিন্ত ভালভাবে ডিজাইন করা কোন অবজেক্ট অরিয়েনটেড প্রোগ্রামে অবজেক্টের ইন্টারনাল অবস্থা জানা বা পরিবর্তন করা সহজ নয়। এর জন্য প্রয়োজন Accessor Methods তথা getters এবং setters।

Objective C তে @property এর কাজ হল অটোমেটিক্যালি এই Accessor method গুলো জেনারেট করে সহজে প্রোপার্টি তৈরী এবং কনফিগার করার ব্যবস্থা করা। যেসব প্রোপার্টি গুলো Public হবে শুধুমাত্র সেগুলোর জন্যই @property ডিরেকটিভ ব্যবহার করা হয়। ফলে সহজেই সিমান্টিক লেভেলে প্রোপার্টির behavior ঠিক করে দেওয়া যায় এবং এটা একই সাথে ওই প্রোপার্টির ইম্পলিমেন্টেশন ডিটেইলসও ম্যানেজ করে।

@property ডিরেকটিভঃ প্রথমেই দেখা যাক @property ডিরেকটিভ ব্যবহার করলে কি ঘটনা ঘটে। গত অধ্যায়ের সাথে মিল রেখে একটি Food ক্লাসের জন্য একটি সহজ ইন্টারফেস এবং ইম্পলিমেন্টেশন লেখা যাক।

//  Food.h

#import <Foundation/Foundation.h>

@interface Food : NSObject

@property NSString *item;

@end
//  Food.m

#import "Food.h"

@implementation Food

@end

Food ইন্টারফেসের একটি প্রোপার্টি item যার ডাটাটাইপ NSString। যেহেতু অবজেকটিভ সি তে সকল ভ্যারিয়েবলকে পয়েন্টার হিসেবে ব্যবহার করা হয় তাই item এর আগে পয়েন্টার চিহ্ন ব্যবহার করা হয়েছে। লক্ষনীয় ব্যপার হল এখানে item ডিক্লেয়ার করার আগে @property ডিরেকটিভ লেখা আছে।

কোন ভ্যারিয়েবলকে @property ডিরেকটিভ হিসেবে ডিক্লেয়ার করা হলে, তার জন্য অটোমেটিক্যালি Accessor methods জেনারেট হয়ে যায় এবং নিজ ক্লাসের ইন্টারফেস ও ইমপ্লিমেন্টেশনে থাকা অন্যান্য মেথড গুলোর মতই ওই অদৃশ্য গেটার এবং সেটার মেথড গুলোকে কল করা যায়। item প্রোপার্টির জন্য আসলে নিচের মত গেটার এবং সেটার মেথড তৈরি হয়ে গেছে যেগুলো আমরা একটু পরেই মুল প্রোগ্রামে ব্যবহার করতে পারবো। বিঃ দ্রঃ কাস্টম সেটার ও গেটার তৈরির করার জন্য Food.m -এ এই গেটার এবং সেটারকে ওভাররাইডও করা যায়। তবে কাস্টম সেটার ও গেটার তৈরির জন্য @synthesize ডিরেকটিভ আবশ্যক।

- (NSString *)item {
    return item;
}
- (void)setItem:(NSString *)newValue {
    item = newValue;
}

এবার আমাদের main.m ফাইলকে নিচের মত করে আপডেট করুন।

// main.m

#import <Foundation/Foundation.h>
#import "Food.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        Food *food = [[Food alloc] init];
        food.item = @"A bengali dish!"; // Under hood: [food setItem: @"A bengali dish!"]

        NSLog(@"Eating a %@", food.item); // Under hood: [food item]

    }
    return 0;
}

ডট নোটেশন দিয়ে প্রোপার্টি এক্সেস করার সময় মুলত ব্যাকএন্ডে accessor মেথড গুলোতেই ট্রানস্লেটেড হয়। ফলে food.item এ কোন স্ট্রিং এসাইন করলে setItem: মেথড কল হয় এবং food.item দিয়ে ডাটা চাইলে item: মেথডটাই কল হয়।

এক্সেসর মেথডগুলোর বিহেভিয়র পরিবর্তন করার জন্য @property ডিরেকটিভ এর পর প্যারেনথেসিস () এর ভিতরে বিহেভিয়র এর ধরন তথা এট্রিবিউট সেট করা যায়। এই চাপ্টারের বাকি অংশে বিভিন্ন রকম অ্যাট্রিবিউট নিয়ে আলোচনা হবে।

getter= এবং setter= অ্যাট্রিবিউটস্ঃ @property ডিরেকটিভ এর ডিফল্ট নেমিং কনভেনশন পছন্দ না হলে, আমরা getter= এবং setter= ব্যবহার করে getter এবং setter মেথড গুলোর নাম পরিবর্তন করতে পারি নিচের মত করে। উল্লেখ্য, গেটার/সেটারকে কাস্টম নাম দেয়ার একটা সাধারণ সিচুয়েশন হচ্ছে যখন কোন বুলিয়ান টাইপ প্রোপার্টির ব্যবহার চলে আসে এবং সেগুলোকে আরও হিউম্যান রিডেবল করার জন্য।

@property (getter=isOpen) BOOL open;

এ অবস্থায় জেনারেট হওয়া এক্সেসর মেথড গুলোকে isOpen এবং setOpen দিয়ে কল করা হবে/যাবে। আসুন আমাদের Food.h, Food.m এবং main.m কে নিচের মত করে সাজাই,

// Food.h

#import <Foundation/Foundation.h>

@interface Food : NSObject

@property (getter=isOpen) BOOL open;

@end
// Food.m

#import "Food.h"

@implementation Food


@end
// main.m

#import <Foundation/Foundation.h>
#import "Food.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        Food *place = [[Food alloc] init];
        place.open = NO; // Under hood: [food setOpen]

        // NSLog(@"Is the restaurant open?: %hhd", [place open]); // Error: No method exists

        NSLog(@"Is the restaurant open?: %hhd", [place isOpen]); // Works!

        NSLog(@"Is the restaurant open?: %hhd", place.open);  // Works! Under hood: [place isOpen]

    }
    return 0;
}

খেয়াল করুন এখন কিন্তু আর গেটারের ব্যাকএন্ড স্টাইল [place open] কাজ করবে না বরং [place isOpen] কাজ করবে যেখানে isOpen কে গেটার হিসেবে বলে দিয়েছি আমরা। আবার, এখনো কিন্তু পাবলিক প্রোপার্টিগুলোকে শুধু প্রোপার্টির নাম দিয়েও এক্সেস করা যাচ্ছে। বিঃ দ্রঃ শুধুমাত্র এই অ্যাট্রিবিউট দুটি আর্গুমেন্ট এক্সেপ্ট করে। অন্য সকল অ্যাট্রিবিউটগুলো বুলিয়ান ফ্ল্যাগ।

রিডঅনলি (readonly) অ্যাট্রিবিউটঃ ক্লাসের কোন প্রোপার্টিকে রিড অনলি (শুধু ভ্যালু রিড করা যাবে, নতুন করে সেট করা যাবে না) করার সহজ উপায় হল এই প্রোপার্টির জন্য readonly অ্যাট্রিবিউট সেট করা। কোন প্রোপার্টিকে readonly করলে, প্রোপার্টিটির setter মেথড থাকে না এবং ডট নোটেশন ব্যবহার করে ভ্যালু অ্যাসাইন করা যায় না। নিচের উদাহরনটি দেখলেই পরিস্কার হয়ে যাবে।

//  Food.h

#import <Foundation/Foundation.h>

@interface Food : NSObject

@property (getter=isOpen, readonly) BOOL open;

@end
//  Food.m

#import "Food.h"

@implementation Food


@end
// main.m

#import <Foundation/Foundation.h>
#import "Food.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        Food *place = [[Food alloc] init];
        place.open = YES; // Error: can't assign to readonly property

    }
    return 0;
}

তবে হ্যাঁ, আমরা ইন্সট্যান্স মেথড ব্যবহার করে ইন্টারনালি এই open নামক রিডঅনলি প্রোপার্টিরটার ভ্যালু পরিবর্তন করতে পারবো। এই সিচুয়েশন ট্রাই করার জন্য নিচে যথাক্রমে ইন্টারফেস, ইমপ্লিমেন্টেশন ও মেইন ফাইলটি দেওয়া হল:

//  Food.h

#import <Foundation/Foundation.h>

@interface Food : NSObject

@property (getter=isOpen, readonly) BOOL open;
-(void)openTheRestaurant;

@end
//  Food.m

#import "Food.h"

@implementation Food

-(void)openTheRestaurant
{
    // N.B. We can use _open directly, cause @property also creates an
    // instance variable for us!
    _open = YES;
}

@end
// main.m

#import <Foundation/Foundation.h>
#import "Food.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Food *place = [[Food alloc] init];
        [place openTheRestaurant]; // Setting the new value by this imethod

        NSLog(@"Is the restaurant open?: %hhd", place.open);
    }
    return 0;
}

এটি রান করালে কনসোলে Is the restaurant open?:1 দেখতে পাবেন অর্থাৎ ওই রিডঅনলি বুলিয়ান প্রোপার্টিটার ভ্যালু চেঞ্জ করা গেছে যেটা বাই ডিফল্ট 0 থাকে।

ননঅ্যাটমিক (nonatomic) অ্যাট্রিবিউটঃ মাল্টিথ্রেডেড এনভাইরনমেন্টে (যখন কোন অ্যাপ্লিকেশনে একাধিক থ্রেড থাকে) getter এবং setter মেথড গুলো একাধিকবার কল হতে পারে। অর্থাত অন্য কোন অপারেশন বর্তমানে এক্সিকিউট হচ্ছে এমন কোন getter/setter মেথডের এক্সিকিউশনে ইন্টারাপ্ট করে রেজাল্টে প্রভাব ফেলতে পারে। Atomic প্রোপার্টি অবজেক্টকে লক করে দেয় যাতে করে getter/setter মেথড গুলো কমপ্লিট ভ্যালু নিয়ে কাজ করতে পারে।

@property ডিরেকটিভ দিয়ে ডিক্লেয়ার করা প্রোপার্টি বাই ডিফল্ট Atomic হয়। যা আমাদের মাথার উপর থেকে ম্যানুয়ালী Atomicity নিশ্চিত করার বোঝা কমিয়ে দিয়েছে। কিন্তু যদি আপনার অ্যাপ্লিকেশনটি একটিমাত্র থ্রেড নিয়ে হয় অথবা আপনি নিজের মত করে Thread-Safety ইমপ্লেমেন্ট করে থাকেন, তাহলে প্রোপার্টিটিকে nonatomic হিসেবে ডিক্লেয়ার করলেই চলে।

@property (nonatomic) NSString *item;

আরও একটি উল্লেখযোগ্য ব্যপার হল, Atomic প্রোপার্টিগুলোর getter এবং setter দুটি মেথডই অটো-জেনারেটেড হবে অথবা দুটি মেথডই ইউজার ডিফাইন্ড হবে। কিন্তু nonatomic প্রোপার্টি গুলোর জন্য কোন একটি ইউজার ডিফাইন্ড এবং অন্যটি অটো জেনারেটেড এরকম মিক্স হতে পারে।

মেমরী ম্যানেজমেন্ট (Memory Management)ঃ অবজেক্ট অরিয়েন্টেড ল্যাঙ্গুয়েজে, অবজেক্ট গুলো কম্পিউটারের মেমরী লোকেশনে স্টোর করা হয়। এখনো পর্যন্ত মেমরীই হল দুর্লভ রিসোর্স বিশেষ করে মোবাইল ডিভাইস গুলোর জন্য। মেমরী ম্যানেজমেন্টের মুল লক্ষ্যই হল কম্পিউটার বা মোবাইলের মেমরীর সঠিক ব্যবহার করা। অ্যাপ্লিকেশনটি অপ্রয়োজনীয় কোন মেমরী যাতে ব্যবহার করতে না পারে সেটা নিশ্চিত করাই মেমরী ম্যানেজমেন্টের কাজ।

বেশীরভাগ ল্যাঙ্গুয়েজেই garbage collection দিয়েই মেমরী ম্যানেজমেন্ট করা হয়। কিন্তু অবজেকটিভ সি (Objective C) অবজেক্ট ওনারশিপ (Object Ownership) দিয়ে মেমরী ম্যানেজ করে যা garbage collection এর চেয়ে অনেকবেশী ইফিশিয়েন্ট। যখন কোন প্রোগ্রাম কোন অবজেক্টের সাথে ইন্টারঅ্যাক্ট শুরু করে তখনই প্রোগ্রামটিকে এই অবজেক্টের ওনার (owner) বলা হয়। আবার যখন প্রোগ্রামটির কাজ শেষ হয়ে যায় অথবা প্রোগ্রামটির আর অবজেক্টের দরকার থাকে না তখন অবজেক্টটির owner হিসেবে প্রোগ্রামটি থাকেনা। একটি অবজেক্টের একাধিক owner থাকতে পারে। অর্থাত যখন কোন অবজেক্টের কমপক্ষে একটি ওনার থাকবে তখন বুঝতে হবে অবজেক্টটি ব্যবহার করা হচ্ছে। কোন অবজেক্টের যদি একটিও Owner না থাকে তখন বুঝতে হবে যে এই অবজেক্টের আর কোন কাজ নেই। তাই অপারেটিং সিস্টেম অবজেক্টটিকে ডেস্ট্রয় করে দিয়ে মেমরী ফ্রি করে নেয়। AUTOMATIC REFERENCE COUNTING এর মাধ্যমে অপারেটিং সিস্টেম নিজে নিজেই মেমরী ম্যানেজমেন্টের কাজ গুলো করে নেয়। এসব নিয়ে আমাদের মাথা গরম করার কোন দরকার নেই। কিন্তু আমাদেরকে অবশ্যই strong, weak এবং copy অ্যাট্রিবিউটগুলো সম্পর্কে জানতে হবে কারন এই অ্যাট্রিবিউটগুলোই কম্পাইলারকে অবজেক্টের রিলাশনশিপ কেমন হবে তা জানায়।

স্ট্রং (strong) এবং উইক (weak) অ্যাট্রিবিউটঃ প্রথমেই দেখি কিভাবে একটি স্ট্রং অ্যাট্রিবিউটের প্রোপার্টি ডিক্লেয়ার করা যায়,

@property (nonatomic, strong) NSString *name;

আগেও একবার বলা হয়েছে যে, ARC বা Automatic Reference Counting হচ্ছে একটি স্বয়ংক্রিয় মেমোরি ম্যানেজমেন্ট ব্যবস্থা যার মাধ্যমে সিস্টেম স্বয়ংক্রিয় ভাবে যেকোনো অপ্রয়োজনীয় অবজেক্টের মেমোরি ফ্রি করে। আর এই ফ্রি করার ব্যপারটা সে ঠিক করে রেফারেন্সের নাম্বার (Count) এর উপর। যেটার রেফারেন্স যখন 0 হয়ে যায় তখনি তার মেমোরি ফ্রি/রিলিজ করে দেয়।

উপরে strong অ্যাট্রিবিউট হচ্ছে কোন অবজেক্টের এমন একটি স্ট্রং রেফারেন্স যা তাকে ডিঅ্যালোকেটেড হতে দেয় না। অর্থাৎ তার রেফারেন্স কাউন্ট হয় 0 এর চেয়ে বেশি বা 1, 2 ... এরকম। এখানে name কে strong প্রোপার্টি হিসেবে ডিক্লেয়ার করা দুটি অর্থ বহন করেঃ ১। যখন আপনি name এর মধ্যে একটি অবজেক্ট অ্যাসাইন করবেন, তখন সেই অবজেক্টটির রেফারেন্স কাউন্ট বেড়ে যায়। ২। যখন অ্যাসাইন করা অবজেক্টটি যাকে name পয়েন্ট করে সেটাকে আপনি বা সিস্টেম কোনভাবে ডিঅ্যালোকেট করবেন তখনও name এর ভ্যালু আগের মতই থাকবে অর্থাৎ name আসলে এর মধ্যে অ্যাসাইন করা অবজেক্টটির মালিকানা পেয়ে গিয়েছিল; কারন name একটি strong Relationship.

এবার আসুন একটি উদাহরণ দেখি। প্রথমেই আমাদের বর্তমান প্রজেক্টে আরও একটি নতুন ক্লাস (NSObject এর সাব) যুক্ত করে নিন যার নাম Person. ফলে নতুন দুটি ফাইল পেয়ে যাবো আমরা যেগুলোর কোড আপডেট করে নিন নিচের মত,

// Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic) NSString *name;

@end

অর্থাৎ এই নতুন ক্লাসের একটি মাত্র সাধারণ প্রোপার্টি যার নাম name.

// Person.m

#import "Person.h"

@implementation Person

- (NSString *)description {
    return self.name;
}

@end

Person ক্লাসের ইমপ্লেমেন্টেশনে আমরা description নামের একটি ডিফল্ট মেথডকে ওভাররাইড করেছি যার কাজ হল এর name প্রোপার্টিটিকে রিটার্ন করা। অর্থাৎ কোথাও এই ক্লাসের ডাইরেক্ট অবজেক্টকেই ব্যবহার করলে তা পক্ষান্তরে এটার name প্রোপার্টিটিকেই সেখানে হাজির করবে। এটুকু আমরা করছি আমাদের উদাহরণ এর স্বার্থে, description মেথডের ব্যবহার শেখাতে নয়।

এখন নিচের মত করে Food.h ফাইল আপডেট করুন,

// Food.h

#import <Foundation/Foundation.h>
#import "Person.h"

@interface Food : NSObject

@property (nonatomic) NSString *item;
@property (nonatomic, strong) Person *cook;

@end

এখানে প্রথমে আমরা একটি সাধারণ NSString প্রোপার্টি অ্যাড করেছি এবং তার নিচে একটি Person প্রোপার্টি অ্যাড করেছি এবং সেটার অ্যাট্রিবিউট হিসেবে strong সেট করেছি। অর্থাৎ পরবর্তীতে এই cook এর মধ্যে যখন আমরা কোন অবজেক্ট অ্যাসাইন করব তখন সেই অবজেক্টটির রেফারেন্স কাউন্ট বেড়ে যাবে।

Food এর ইমপ্লেমেন্টেশন একদমই ফাকা আপাতত এই উদাহরণ এর সাপেক্ষে,

// Food.m

#import "Food.h"

@implementation Food


@end

এবার main.m কে নিচের মত আপডেট করুন,

// main.m

#import <Foundation/Foundation.h>
#import "Food.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        Person *tomi = [[Person alloc] init];
        tomi.name = @"Tomi Mia";

        Food *newDish = [[Food alloc] init];
        newDish.item = @"Halim";
        newDish.cook = tomi;

        NSLog(@"%@ is cooking the %@", newDish.cook, newDish.item);

    }
    return 0;
}

খেয়াল করুন, প্রথমে আমরা Person ক্লাস ইন্সট্যান্সিয়েট করে tomi অবজেক্টের মাধ্যমে এর (Person এর) name প্রোপার্টিটি সেট করে দিয়েছি। তারপর আমরা Food ক্লাস ইন্সট্যান্সিয়েট করে newDish অবজেক্টের মাধ্যমে এর (Food এর) item প্রোপার্টিটি সেট করে দিয়েছি। অতঃপর, newDish অবজেক্টের মাধ্যমে এর cook নামের স্ট্রং প্রোপার্টিটির ভেতর আমাদের একটু আগে তৈরি করা tomi অবজেক্টটিকে অ্যাসাইন করেছি। এই প্রোগ্রামকে রান করালে কনসোলে আউটপুট আসবে "Tomi Mia is cooking the Halim"। যেহেতু, cook একটি strong relationship তাই newDish অবজেক্টটি tomi এর ownership নিয়ে নেয় এবং এটা ততক্ষণ পর্যন্ত ভ্যালিড থাকে যতক্ষণ পর্যন্ত tomi কে newDish এর প্রয়োজন হয়।

এখন সবকিছু ঠিক রেখেই শুধুমাত্র main.m কে নিচের মত করে আপডেট করুন,

// main.m

#import <Foundation/Foundation.h>
#import "Food.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        Person *tomi = [[Person alloc] init];
        tomi.name = @"Tomi Mia";

        Food *newDish = [[Food alloc] init];
        newDish.item = @"Halim";
        newDish.cook = tomi;

        tomi = nil; // Trying to deallocating the "tomi" object

        NSLog(@"%@ is cooking the %@", newDish.cook, newDish.item);

    }
    return 0;
}

এখানে আমরা একটা ধাপে tomi অবজেক্টকে ম্যানুয়ালি nil করেছি কিন্তু প্রোগ্রামটি রান করে দেখুন আউটপুট আগের মতই আসবে, "Tomi Mia is cooking the Halim"।

কিন্তু,

এবার নিচের মত করে Food.h ফাইলকে আপডেট করুন,

// Food.h

#import <Foundation/Foundation.h>
#import "Person.h"

@interface Food : NSObject

@property (nonatomic) NSString *item;
@property (nonatomic, weak) Person *cook;

@end

অর্থাৎ cook কে weak করে ফেলুন এবং main.m রাখুন একটু আগে যেমন ছিল তেমনি,

// main.m

#import <Foundation/Foundation.h>
#import "Food.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        Person *tomi = [[Person alloc] init];
        tomi.name = @"Tomi Mia";

        Food *newDish = [[Food alloc] init];
        newDish.item = @"Halim";
        newDish.cook = tomi;

        tomi = nil; // Trying to deallocating the "tomi" object

        NSLog(@"%@ is cooking the %@", newDish.cook, newDish.item);

    }
    return 0;
}

এবার প্রোগ্রামটিকে রান করালে আউটপুট আসবে "(null) is cooking the Halim"। অর্থাৎ tomi অবজেক্ট ডিঅ্যালোকেট হবার পর newDish এর weak প্রোপার্টি cook, tomi কে হারিয়ে ফেলছে। কিন্তু ১৮ নাম্বার লাইনটি মুছে ফেলে দেখুন, কনসোলে আগের মত আউটপুট আসবে।

আশা করি স্ট্রং এবং উইক প্রোপার্টি এর ব্যপার গুলো একটু হলেও বোঝানো গেছে। আমাদের সম্ভাব্য বাংলা কাগুজে বইয়ে আরও বিস্তারিত আলোচনা থাকবে এই ব্যাপারে।

কপি (copy) অ্যাট্রিবিউটঃ এটি মোটামুটি strong এর মতই কিন্তু বিশাল একটা পার্থক্য আছে। স্ট্রং এর মত সরাসরি অবজেক্টের ownership না নেয়ার বদলে এটি প্রথমে, ওই প্রোপার্টির মধ্যে যা অ্যাসাইন করা হয় সেটার একটা কপি তৈরি করে এবং সেই কপিটির ownership নিয়ে রাখে। যে ধরনের প্রোপার্টি গুলো ভ্যালু রিপ্রেজেন্ট করে সেগুলোই সাধারণত copy হিসেবে ডিক্লেয়ার করা হয় যেমন NSString টাইপের প্রোপার্টি গুলো।

@property (nonatomic, copy) NSString *item;

আরও যেসব অ্যাট্রিবিউট রয়েছেঃ

retain

unsafe_unretained

assign

Originally Posted Here

Last updated