1. Giriş
Əgər artıq hər sahəyə uyğun üslubda getName()/setName() kodunu görmüsünüzsə, asanlıqla deyə bilərsiniz: «Yaxşı, belə qəbul edilib». Sonra isə C++ kodu ilə rastlaşa bilərsiniz ki, orada getX() əvəzinə sadəcə x() yazırlar, və setX() əvəzinə — məsələn moveTo(...), və artıq niyə belə olduğunu anlamaq çətinləşir.
Burada əsas fikir odur: getterlər və setterlər sehr deyil və ya «böyük proqramçı ritualı» deyil, həqiqətən adi metodlardır. Onların dəyəri o vaxt ortaya çıxır ki, onlar invariantları dəstəkləməyə və əməliyyatların mənasını ifadə etməyə kömək etsin. Əgər onlar sadəcə sahələrə eyni səviyyədə giriş verirsə, siz səs-küylü interfeys alırsınız ki, bu da «enkapsulyasiya» illuziyasını yaradır, amma əslində heç nəyi qorumur — və tez-tez sinfi ağrısız şəkildə inkişaf etdirməyə mane olur.
Danışıq abstrakt olmasın deyə, tədris konsol tətbiqimiz TaskTracker (mini vəzifə-tracker) üzərində davam edək. Əvvəllər (struct mərhələsində) vəzifə belə görünə bilərdi: id, title, done. İndi biz «ağıllı obyekt»ə doğru addım atmaq istəyirik — obyekt xaricdən özünü poza bilməsin.
2. Getterlər: «oxumağa vermək» — həm də məsuliyyətdir
«Getter» dedikdə adətən belə təsəvvür edirik: «bu sahənin dəyərini almaq üçün». Amma sinif dizaynında getter yalnız oxumağa görə deyil, həm də xarici kodun məlumatı hansı formada görəcəyinə görə cavabdehdir.
Bu gözlənilməz dərəcədə vacibdir: eyni faktı təhlükəsiz və ya təhlükəli, ucuz və ya baha, rahat və ya əlverişsiz şəkildə «vermək» olar. C++-da bir neçə variantımız var: qiymətcə qaytarmaq, const T& ilə qaytarmaq, və bəzən (çox ehtiyatla) T& ilə qaytarmaq. Və məhz getterlərdə enkapsulyasiyanın ilk ciddi çatlaqları tez-tez başlayır, çünki «mən sadəcə rahatlıq istədim» asanlıqla «mən təsadüfən daxili idarəetmə qolunu verdim»ə çevrilir.
Kiçik tiplər üçün sadə və təhlükəsiz getter
int, bool kimi kiçik tiplər üçün demək olar ki, həmişə qiymətcə qaytarmaq rahat və təhlükəsizdir:
class Task {
public:
int id() const { return id_; }
bool is_done() const { return done_; }
private:
int id_ = 0;
bool done_ = false;
};
Burada gizli tuzak yoxdur: int və bool sürətlə kopyalanır, xaricə birbaşa daxili sahələrə giriş vermirsiniz və invariantlar zərər görmür.
std::string üçün getter: qiymət yoxsa const&
Sətirlərdə vəziyyət daha maraqlıdır. Vəzifənin başlığı varsa, belə etmək olar:
#include <string>
class Task {
public:
std::string title() const { return title_; } // kopya
private:
std::string title_ = "untitled";
};
Bu təhlükəsizdir, amma bahalı ola bilər: sətir kopyalanır. Bəzən bu normaldır (sətirlər qısadır, çağırlmalar azdır). Bəzən isə kopya olmadan oxumaq istəyirsən — onda const-istiqamətli referans qaytarırlar:
#include <string>
class Task {
public:
const std::string& title() const { return title_; } // kopya yoxdur
private:
std::string title_ = "untitled";
};
Bu hələ də enkapsulyasiya üçün təhlükəsizdir, çünki const std::string& xarici koda sətiri birbaşa dəyişmək imkanı vermir. Amma vacib nüans yaranır: referans obyektə «bağlanır». Əgər obyekti məhv etsəniz, referans sarkmış (dangling) olur.
Qaydası sadədir: const&-getterin nəticəsini obyektin özündən daha uzun müddət saxlamayın. Əgər onu dərhal istifadə edirsinizsə — hər şey yaxşıdır.
Niyə T&-getter tez-tez sinfi pozur
Budur «çox rahat» görünən getter:
#include <string>
class Task {
public:
std::string& title() { return title_; } // təhlükəli
private:
std::string title_ = "untitled";
};
İndi xarici kod istədiyini edə bilər:
Task t;
// Xarici kod dəyişdirilə bilən refersiya alıb və bütün yoxlamaları keçdi.
t.title().clear();
Əgər sizin invariant «vəzifənin başlığı boş olmamalıdır» idisə, o indi pozuldu. Formal olaraq sinif «enkapsulyasiya olunub» (sahə private), amma faktiki olaraq daxili hissəyə birbaşa kabel vermisiniz. Buna «enkapsulyasiya sızması» deyilir.
Qısa cədvəl: getterdə qaytarma tipini necə seçmək
Sinif yazmağa başlayanda sadə qayda faydalıdır. Bu «qanun» deyil, amma hər metodda beynin ilişməməsi üçün sağlam məntiq verir.
| Nə qaytarırıq | Nə vaxt uyğundur | Nə qazanırsınız | Nə səhv ola bilər |
|---|---|---|---|
| T (qiymətcə) | kiçik tiplər (int, bool) və ya kopya qorxutmursa | sadəlik və təhlükəsizlik | böyük obyektlər üçün baha ola bilər |
| const T& | böyük tiplər (std::string, std::vector) və tez-tez oxumaq | kopya yoxdur, hələ də təhlükəsizdir | referansı obyektdən uzun saxlamamaq lazımdır |
| T& | nadir: xarici koda daxili hissəni dəyişmək üçün şüurlu icazə verdiyiniz zaman | maksimum «rahatlıq» | demək olar ki, həmişə invariantları və dizaynı pozur |
Qeyd edin: C++-da getter API-nin bir hissəsidir. Bir dəfə T& verdinizsə, demək olar ki, istifadəçiyə vədə verdiniz ki, bunu həmişə birbaşa dəyişmək olar. Sonra siz yoxlama əlavə etmək istəyəndə… məlum olacaq ki, yoxlamanı artıq T&-dən yan keçərək məğlub edirlər.
3. Setterlər və əməliyyatlar: invariantları itirmədən vəziyyəti necə dəyişmək
Setter sadə görünür: «sahəyə dəyər təyin et». Amma normal sinifdə setter — nəzarət nöqtəsi-dir: burada siz qərar verirsiniz, dəyişiklik icazəlidirmi, məlumatı normalizə etmək lazımdırmı, imtina etmək lazımdırmı və invariantları necə saxlamaq.
Əsas meyar: setter o vaxt haqlıdır ki, o qaydaları və mənanı ifadə etsin. Əgər o sadəcə olaraq field_ = value; edirsə, yoxlamasız, o tez-tez «public sahə, amma iki əlavə ekran kodla» olur.
Qaydalar olmayan setter: formal məqam var, amma faktiki heç nə qorunmur
#include <string>
class TaskBad {
public:
void set_title(const std::string& title) {
title_ = title; // yoxlamasız
}
const std::string& title() const { return title_; }
private:
std::string title_ = "untitled";
};
Burada pis nədir? Siz «girişə nəzarət edirəm» elan etdiniz, amma əslində nəzarət etmirsiniz. Xarici kod boş sətrə, yalnız boşluqlardan ibarət sətrə, çox uzun sətrə — hər nə istəsə təyin edə bilər. Sinif özünü qorumur.
Yoxlamalı setter: «xarakterli setter»
İnvariant tətbiq edək: vəzifənin başlığı boş olmamalıdır.
#include <string>
class Task {
public:
bool set_title(const std::string& title) {
if (title.empty()) return false;
title_ = title;
return true;
}
const std::string& title() const { return title_; }
private:
std::string title_ = "untitled";
};
İndi setter əslində faydalıdır: o sadəcə təyin etmir, həm də obyektin səhv olmasına yol vermir.
Stilə diqqət edin: biz bool qaytarırıq ki, xarici kod üçün uğurlu olub-olmadığı görünsün. Biz istisnalardan istifadə etmirik (bu ayrıca böyük mövzudur), buna görə «neat imtina» normal təcrübədir.
«Hər sahəyə get/set» sinfi anemik edir
Yeni başlayanların ən çox rast gəldiyi səhv belə səslənir: «Class və private varsa, gəlin hər sahəyə get/set edək». Ciddi görünür, amma əgər set_* daxilində məntiq və qoruma yoxdursa, sinif sadəcə səs-küylü struct-a çevrilir.
O məna əlavə etməyib, amma öhdəliklər yaradır: indi xarici kod onlarla bağlı olur və daxili təmsili dəyişdirmək çətinləşir.
Tutaq ki, vəzifənin prioriteti 1-dən 5-ə qədərdir. Əgər set_priority(int p) yoxlamasız yazmısınızsa, -100 və ya 999 qoymağa imkan verirsiniz. Proqramın digər yerlərində belə «pətəklər» yaranacaq: if (task.priority() < 1) ..., və siz uduzursunuz: yoxlama bütün kod boyu yayıldı, sinif isə artıq «məyusçu» deyil.
Setterlər əvəzinə əməliyyat-metodlar
Faydalı vərdiş: setter yazmaq istəyəndə dayanın və özünüzdən soruşun: «Bu həqiqətən sahənin təyini midir, yoxsa əməliyyatıdır?». Çox hallarda daha oxunaqlı və təhlükəsizdir ki, əməliyyat-metod verəsiniz.
Məsələn, set_done(true) əvəzinə tez-tez daha yaxşıdır:
- mark_done()
- reopen()
Bu əmrlər kimi oxunur, hansı dəyərin nə olduğunu xatırlamağa ehtiyac yoxdur və içəridə invariantları dəstəkləmək mümkündür.
4. Praktik nümunə: əlavə get/set olmadan TaskTracker-i yaxşılaşdırmaq
İndi TaskTracker üçün daha realistik hissə toplayaq. Gəlin vəzifənin belə sahələri olsun:
- id (yaradıldıqdan sonra dəyişmir),
- title (boş olmamalıdır),
- done (dəyişir, amma əməliyyat-metodlar vasitəsilə),
- priority (1..5).
Minimal «düzgün» Task sinfi
#include <string>
class Task {
public:
Task(int id, const std::string& title, int priority)
: id_(id), title_(title), priority_(priority) {
// Bu mühazirədə konstruktorlara dərindən girmirik, amma fikri artıq gördünüz.
// Yoxlamalar burada da edilə bilər, amma indi API-yə fokuslanırıq.
if (title_.empty()) title_ = "untitled";
if (priority_ < 1) priority_ = 1;
if (priority_ > 5) priority_ = 5;
}
int id() const { return id_; }
const std::string& title() const { return title_; }
int priority() const { return priority_; }
bool is_done() const { return done_; }
bool rename(const std::string& new_title) {
if (new_title.empty()) return false;
title_ = new_title;
return true;
}
bool set_priority(int p) {
if (p < 1 || p > 5) return false;
priority_ = p;
return true;
}
void mark_done() { done_ = true; }
void reopen() { done_ = false; }
private:
int id_ = 0;
std::string title_ = "untitled";
int priority_ = 1; // invariant: 1..5
bool done_ = false;
};
Burada fikirə diqqət yetirin: biz set_id etmədik, çünki id — vəzifənin identifikatorudur. Xarici kod id-ni dəyişə bilsə, tezliklə xaos yaranar: vəzifə «dünən #3 idi, bugün #17». Bəzən belə etmək olar, amma o halda bu artıq id deyil, «siyahıdakı sıra nömrəsi» olacaq və bu başqa məfhumdur.
Kiçik main()-də sinifdən istifadə
#include <iostream>
#include <string>
int main() {
Task t(1, "Buy milk", 3);
std::cout << t.id() << ": " << t.title() << "\n"; // 1: Buy milk
t.mark_done();
std::cout << t.is_done() << "\n"; // 1
if (!t.rename("")) {
std::cout << "Bad title!\n"; // Bad title!
}
}
Bəli, bir az daha uzun görünə bilər, birbaşa sahəyə girişdən. Amma indi «düzgünlük» Task daxilində cəmlənib, proqramın hər yerinə yayılmayıb.
5. Enkapsulyasiya sızması: zərərsiz görünən nümunə
Bəzən «sürətləndirmək» və «sadələşdirmək» istəyirsiniz: məsələn, vəzifənin başlığına dəyişdirilə bilən referans verib birbaşa düzəliş etmək. Göstərək niyə bu invariantlar kontekstində təhlükəlidir.
Pis variant:
#include <string>
class TaskLeaky {
public:
std::string& title() { return title_; } // xaricə idarəni verdik
const std::string& title() const { return title_; }
private:
std::string title_ = "untitled"; // invariant: boş olmamalıdır
};
İstifadə:
#include <iostream>
int main() {
TaskLeaky t;
t.title().clear(); // invariant pozuldu
std::cout << "[" << t.title() << "]\n"; // []
}
Sinif artıq «boş olmamalıdır» qaydasını təmin edə bilmir, çünki siz özü-özünüzə yan keçən tunel yaratdınız.
Düzgün variant — yalnız oxumağa icazə verin və dəyişiklik üçün metod saxlayın:
#include <string>
class TaskSafe {
public:
const std::string& title() const { return title_; }
bool rename(const std::string& new_title) {
if (new_title.empty()) return false;
title_ = new_title;
return true;
}
private:
std::string title_ = "untitled";
};
İndi dəyişmək mümkündür, amma yalnız rename nəzarət nöqtəsi vasitəsilə — yəni yoxlama ilə.
6. Diaqram: sinif interfeysi vəziyyəti necə qoruyur
Bəzən ideyanı «gözlə» görmək faydalıdır:
flowchart TD
A[Xarici kod] -->|publik metodları çağırır| B[Sinifin publik interfeysi]
B -->|yoxlamalar və qaydalar| C[Private sahələr]
A -->|əgər T& xaricə verilibsə| C
Mənası sadədir: vəziyyəti dəyişməyin normal yolu publik interfeysdir, orada yoxlamalar yaşayır. Amma əgər siz daxiliyə dəyişdirilə bilən referans (T&) vermisinizsə — xarici kod birbaşa «gəzib», və sizin qaydalar işə yaramır.
7. Getter və setter adları: getTitle() vs title()
Müxtəlif kod bazalarında müxtəlif üslublarla rastlaşacaqsınız. Tədris nümunələrində tez-tez belə stil istifadə edirik: title() / priority() / is_done() — qısa və «xüsusiyyət» kimi oxunur. getTitle() stili də pis deyil, xüsusən komandada belə razılıq varsa.
Daha vacibi: metodun adı mənanı əks etdirməlidir. Əgər metod set_title adlanır, amma içəridə sətiri normalizə edir (boşluqları kəsir, boşu «untitled» ilə əvəz edir), ola bilər ki, onu rename və ya try_set_title adlandırmaq məntiqli olsun ki, davranış gözlənilən olsun.
Metodu «set» adlandırıb, sonra içəridə «əgər yoxdursa əlavə et» etmək də anlaşılmazlığa səbəb olur.
Getterlər və setterlər nə vaxt həqiqətən uyğundur
Bəzi ssenarilər var ki, getterlər və setterlər mükəmməl alətdir və onları şeytanlaşdırmaq lazım deyil.
Getter demək olar həmişə uyğundur, əgər sahə privatedirsə və siz təhlükəsiz oxumaq verməlisiniz. Setter uyğundur, əgər sahənin «tə’yin rejimi» var və düzgünlük qaydaları mövcuddur. Bizim Task-də bu set_priority-dir, çünki «prioritet» tənzimlənə bilən dəyərdir və dəqiq diapazonu var. Eyni zamanda done üçün biz mark_done/reopen əməliyyatlarını seçdik — çünki belə daha aydın oxunur və mənanı qarışdırmaq ehtimalı azdır.
8. Tipik səhvlər
Səhv №1: «hər sahəyə get və set, çünki belə qəbul edilib».
Belə yanaşma çox vaxt sinifi səs-küylü və yararsız edir: invariantlar qorunmur, məntiq yoxdur, xarici kod çox sayda kiçik metoda bağlı olur. Əgər siz məna və qaydalar əlavə etmirsinizsə, daha yaxşıdır struct saxlayasınız (hər şey açıq görünür), yoxsa həqiqi siniflə əməliyyatlar və yoxlamalar edin.
Səhv №2: yoxlamasız setterlər, səhv dəyərləri buraxır.
Metod set_* adlananda oxucu gözləyir ki, sonra obyekt düzgün qalacaq. Əgər siz «hər nə olsa qəbul et» demisinizsə, yoxlamalar layihə boyu çoxalacaq və sinif invariant qoruyucusu rolunu itirəcək. Daha yaxşıdır rədd etmək (məsələn, false qaytarmaq) nəinki «susaraq pis dəyəri qəbul etmək».
Səhv №3: getter T& qaytarır, çünki «belə rahatdır».
Bu klassik enkapsulyasiya sızmasıdır: xarici kod daxili hissələri sizin qaydalarınız olmadan dəyişdirə bilər. Xüsusilə təhlükəlidir std::string& və ya std::vector<T>& vermək, çünki xarici tərəf konteyneri boşalda, qəribə elementlər əlavə edə və sahələr arasındakı əlaqəni poza bilər. Əgər kopya olmadan oxumaq lazımdırsa — const T& qaytarın. Əgər dəyişiklik lazımdırsa — əməliyyatı ifadə edən və yoxlamalar saxlayan metod yazın.
Səhv №4: setterlər obyektin «identikliyini» dəyişdirir.
set_id(...)-tipli metodlar yaxşı görünə bilər, amma obyektləri konteynerdə saxlayanda, id-yə görə axtaranda, hesabatlar çap edərkən və məlumatları bağlayanda problem yarana bilər. Əgər id dəyişirsə, tətbiq səviyyəsində uyğunluq asanlıqla pozulur. Adətən identiklik yaradılarkən təyin olunur və sonra dəyişdirilmir.
Səhv №5: getterlər referans qaytarır, amma çağıran kod onu çox uzun saxlayır.
const std::string& qaytarmaq rahat və sürətlidir, amma referans yalnız obyekt yaşadığı müddətcə etibarlıdır. Əgər onu saxlayıb sonra obyekt məhv edilirsə və ya əvəz edilirsə, artıq mövcud olmayan məlumatlara istinad edəcəksiniz. Tədris nümunələrində daha rahat qayda: getterdən alınan referansı dərhal istifadə edin və «sonra üçün» saxlamayın; saxlamalısınızsa — kopya alın.
GO TO FULL VERSION