Nesne Yönelimli Bir Tasarım Desenini Uygulamak
Durum deseni (state pattern), nesne yönelimli tasarım desenlerinden biridir. Temel fikir şudur: Bir değerin içsel olarak bulunabileceği bir durum kümesi tanımlanır. Bu durumlar ayrı durum nesneleri ile temsil edilir ve değerin davranışı, içinde bulunduğu duruma göre değişir.
Bir blog gönderisi örneği üstünden gidelim. Gönderinin durumu taslak,
inceleme bekleyen veya yayınlanmış olabilir. İstediğimiz son davranış
şudur:
- Boş bir taslak gönderi oluşturabilmeliyiz.
- Taslak gönderiye metin ekleyebilmeliyiz.
- İnceleme isteyebilmeliyiz.
- Onay verebilmeliyiz.
- Yalnızca yayınlandıktan sonra içerik okunabilmeli.
18-11 numaralı liste, kullanmak istediğimiz API’yi gösteriyor.
use gunce::Gonderi;
fn main() {
let mut post = Gonderi::yeni();
post.metin_ekle("Bugun ogle yemeginde salata yedim");
assert_eq!("", post.icerik());
post.inceleme_iste();
assert_eq!("", post.icerik());
post.onayla();
assert_eq!("Bugun ogle yemeginde salata yedim", post.icerik());
}
Bu tür, durum desenini kullanacak. İçinde üç olası durumdan birini temsil eden
bir değer taşıyacak: Taslak, IncelemeBekleyen veya Yayinlanmis.
Durum geçişleri Gonderi türünün içinde yönetilecek; kütüphaneyi kullanan
kişinin bunları doğrudan yönetmesi gerekmeyecek.
Gonderiyi Tanımlamak ve Yeni Bir Örnek Oluşturmak
Önce içerik tutan açık bir Gonderi struct’ı ve onun yeni örneğini oluşturan
ilişkili bir yeni fonksiyonu tanımlayalım. Aynı zamanda, gönderi durumlarının
ortak davranışını belirleyen gizli bir Durum trait’i de tanımlayacağız.
Gonderi, durum adlı gizli alanında Option<Box<dyn Durum>> tutacak.
Option<T> kullanımının neden gerekli olduğunu birazdan göreceğiz.
pub struct Gonderi {
durum: Option<Box<dyn Durum>>,
icerik: String,
}
impl Gonderi {
pub fn yeni() -> Gonderi {
Gonderi {
durum: Some(Box::new(Taslak {})),
icerik: String::new(),
}
}
}
trait Durum {}
struct Taslak {}
impl Durum for Taslak {}
Gonderi struct’ı, yeni örnek üreten yeni fonksiyonu, Durum trait’i ve Taslak struct’ıDurum trait’i, gönderinin farklı durumlarının ortak davranış yüzeyini
belirler. Şimdilik içinde metot yok; önce yalnızca gönderinin başlangıç durumu
olan Taslakı tanımlıyoruz.
Yeni bir Gonderi oluşturduğumuzda, durum alanını Taslak örneğini tutan
bir Box ile Some yapıyoruz. Böylece her yeni gönderi otomatik olarak taslak
başlar. durum alanı gizli olduğu için, dışarıdan başka bir durumda Gonderi
oluşturmak mümkün değildir.
Gönderi İçeriğini Saklamak
18-11 numaralı listedeki kullanıma göre, gönderiye metin eklemek için
metin_ekle adında bir metodumuz olmalı. Bunu doğrudan icerik alanını pub
yaparak değil metod ile sunuyoruz; çünkü birazdan içeriğin hangi koşullarda
okunacağını biz denetlemek isteyeceğiz.
pub struct Gonderi {
durum: Option<Box<dyn Durum>>,
icerik: String,
}
impl Gonderi {
// --snip--
pub fn yeni() -> Gonderi {
Gonderi {
durum: Some(Box::new(Taslak {})),
icerik: String::new(),
}
}
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
}
trait Durum {}
struct Taslak {}
impl Durum for Taslak {}
icerik alanına metin ekleyen metin_ekle metodunu uygulamakmetin_ekle, self için değiştirilebilir referans alır; çünkü gönderiyi
değiştirir. Verilen metni icerik dizgisinin sonuna ekler. Bu davranış gönderi
durumundan bağımsız olduğu için durum deseninin bir parçası değil, Gonderinin
genel API’sinin bir parçasıdır.
Taslak Gönderinin İçeriğinin Boş Görünmesini Sağlamak
metin_ekle ile içerik eklesek bile, gönderi hâlâ taslak durumunda olduğu için
icerik metodunun boş dizgi döndürmesini istiyoruz. Şimdilik en basit
uygulamayla başlayalım: her zaman boş dizgi döndürsün.
pub struct Gonderi {
durum: Option<Box<dyn Durum>>,
icerik: String,
}
impl Gonderi {
// --snip--
pub fn yeni() -> Gonderi {
Gonderi {
durum: Some(Box::new(Taslak {})),
icerik: String::new(),
}
}
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
pub fn icerik(&self) -> &str {
""
}
}
trait Durum {}
struct Taslak {}
impl Durum for Taslak {}
icerik metoduBu haliyle 18-11 numaralı listedeki ilk assert_eq! beklendiği gibi çalışır.
İnceleme İstemek, Yani Gönderi Durumunu Değiştirmek
Sıradaki adım, gönderi için inceleme isteyebilmek. Bu, durumu Taslaktan
IncelemeBekleyene taşımalı.
pub struct Gonderi {
durum: Option<Box<dyn Durum>>,
icerik: String,
}
impl Gonderi {
// --snip--
pub fn yeni() -> Gonderi {
Gonderi {
durum: Some(Box::new(Taslak {})),
icerik: String::new(),
}
}
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
pub fn icerik(&self) -> &str {
""
}
pub fn inceleme_iste(&mut self) {
if let Some(s) = self.durum.take() {
self.durum = Some(s.inceleme_iste())
}
}
}
trait Durum {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum>;
}
struct Taslak {}
impl Durum for Taslak {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
Box::new(IncelemeBekleyen {})
}
}
struct IncelemeBekleyen {}
impl Durum for IncelemeBekleyen {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
self
}
}
Gonderi ve Durum trait’i için inceleme_iste metodunu uygulamakGonderi, inceleme_iste adında açık bir metot alıyor. Bu metot, mevcut duruma
ait iç inceleme_iste metodunu çağırıyor. O iç metot mevcut durumu tüketip yeni
bir durum döndürüyor.
Burada önemli nokta şu: Durum trait’indeki metodun ilk parametresi self,
&self ya da &mut self değil; self: Box<Self>. Bu sayede eski durumun
sahipliğini alıp onu yeni bir duruma dönüştürebiliyoruz.
Tam da bu yüzden Gonderi içindeki durum alanını Option içine koymuştuk.
take çağrısı Some içindeki değeri alıp yerine None bırakır; böylece eski
durumu ödünç almak yerine gerçekten taşıyabiliriz. Sonra alanı yeni durumla
yeniden doldururuz.
Taslak için inceleme_iste, yeni bir IncelemeBekleyen üretir. Buna karşılık
zaten IncelemeBekleyen durumda olan bir gönderide aynı metodu çağırırsanız,
durum değişmeden aynı halde kalır.
onayla Metodunu Ekleyip icerik Davranışını Değiştirmek
onayla metodu da benzer şekilde çalışır: mevcut durum “onaylandığında”
hangi yeni duruma dönüşmesi gerekiyorsa ona geçer.
pub struct Gonderi {
durum: Option<Box<dyn Durum>>,
icerik: String,
}
impl Gonderi {
// --snip--
pub fn yeni() -> Gonderi {
Gonderi {
durum: Some(Box::new(Taslak {})),
icerik: String::new(),
}
}
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
pub fn icerik(&self) -> &str {
""
}
pub fn inceleme_iste(&mut self) {
if let Some(s) = self.durum.take() {
self.durum = Some(s.inceleme_iste())
}
}
pub fn onayla(&mut self) {
if let Some(s) = self.durum.take() {
self.durum = Some(s.onayla())
}
}
}
trait Durum {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum>;
fn onayla(self: Box<Self>) -> Box<dyn Durum>;
}
struct Taslak {}
impl Durum for Taslak {
// --snip--
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
Box::new(IncelemeBekleyen {})
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
self
}
}
struct IncelemeBekleyen {}
impl Durum for IncelemeBekleyen {
// --snip--
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
self
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
Box::new(Yayinlanmis {})
}
}
struct Yayinlanmis {}
impl Durum for Yayinlanmis {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
self
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
self
}
}
Gonderi ve Durum trait’i için onayla metodunu uygulamakArtık Yayinlanmis adında yeni bir durumumuz var. Taslak için onayla
çağrısı etkisizdir ve durumu değiştirmez. IncelemeBekleyen için onayla
çağrısı ise Yayinlanmis duruma geçer. Yayinlanmis durumda hem
inceleme_iste hem onayla çağrıları yine aynı durumu korur.
Şimdi Gonderi::icerik metodunun davranışını gerçek duruma göre belirlemek
istiyoruz. Bunun için Gonderi, içindeki durum nesnesine bu sorumluluğu
devredecek.
pub struct Gonderi {
durum: Option<Box<dyn Durum>>,
icerik: String,
}
impl Gonderi {
// --snip--
pub fn yeni() -> Gonderi {
Gonderi {
durum: Some(Box::new(Taslak {})),
icerik: String::new(),
}
}
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
pub fn icerik(&self) -> &str {
self.durum.as_ref().unwrap().icerik(self)
}
// --snip--
pub fn inceleme_iste(&mut self) {
if let Some(s) = self.durum.take() {
self.durum = Some(s.inceleme_iste())
}
}
pub fn onayla(&mut self) {
if let Some(s) = self.durum.take() {
self.durum = Some(s.onayla())
}
}
}
trait Durum {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum>;
fn onayla(self: Box<Self>) -> Box<dyn Durum>;
}
struct Taslak {}
impl Durum for Taslak {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
Box::new(IncelemeBekleyen {})
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
self
}
}
struct IncelemeBekleyen {}
impl Durum for IncelemeBekleyen {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
self
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
Box::new(Yayinlanmis {})
}
}
struct Yayinlanmis {}
impl Durum for Yayinlanmis {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
self
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
self
}
}
Gonderi::icerik metodunu Durum trait’indeki icerik metoduna devretmekAmaç, icerik ile ilgili bütün kuralları Durum trait’ini uygulayan türlerin
içine toplamaktır. Bunun için Durum trait’ine de bir icerik metodu ekleriz.
pub struct Gonderi {
durum: Option<Box<dyn Durum>>,
icerik: String,
}
impl Gonderi {
pub fn yeni() -> Gonderi {
Gonderi {
durum: Some(Box::new(Taslak {})),
icerik: String::new(),
}
}
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
pub fn icerik(&self) -> &str {
self.durum.as_ref().unwrap().icerik(self)
}
pub fn inceleme_iste(&mut self) {
if let Some(s) = self.durum.take() {
self.durum = Some(s.inceleme_iste())
}
}
pub fn onayla(&mut self) {
if let Some(s) = self.durum.take() {
self.durum = Some(s.onayla())
}
}
}
trait Durum {
// --snip--
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum>;
fn onayla(self: Box<Self>) -> Box<dyn Durum>;
fn icerik<'a>(&self, post: &'a Gonderi) -> &'a str {
""
}
}
// --snip--
struct Taslak {}
impl Durum for Taslak {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
Box::new(IncelemeBekleyen {})
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
self
}
}
struct IncelemeBekleyen {}
impl Durum for IncelemeBekleyen {
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
self
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
Box::new(Yayinlanmis {})
}
}
struct Yayinlanmis {}
impl Durum for Yayinlanmis {
// --snip--
fn inceleme_iste(self: Box<Self>) -> Box<dyn Durum> {
self
}
fn onayla(self: Box<Self>) -> Box<dyn Durum> {
self
}
fn icerik<'a>(&self, post: &'a Gonderi) -> &'a str {
&post.icerik
}
}
Durum trait’ine icerik metodunu eklemekBu metot için varsayılan uygulama boş dizgi döndürür. Böylece Taslak ve
IncelemeBekleyen durumlarında ayrıca icerik yazmamıza gerek kalmaz.
Yayinlanmis ise bu varsayılanı geçersiz kılıp gerçek gönderi içeriğini
döndürür.
Bu noktada 18-11 numaralı listedeki bütün davranışlar çalışır. Durum desenini
uygulamış olduk ve kurallar Gonderi içine dağılmak yerine durum nesnelerinin
içinde toplandı.
Neden Enum Değil?
“Durumlar için neden enum kullanmadık?” diye düşünebilirsiniz. Elbette bu da
mümkündür. Ama enum kullandığınızda, her kontrol noktasında bütün varyantları
ele alan match ifadeleri yazmanız gerekir. Bu da bazı durumlarda trait
nesnesi çözümünden daha tekrar eden bir yapıya dönüşebilir.
Durum Deseninin Artılarını ve Eksilerini Değerlendirmek
Bu tasarımın güçlü yanı, Gonderinin farklı durumlardaki davranışlarının
durum nesneleri içinde kapsüllenmesidir. Gonderi metotlarının ya da Gonderi
kullanan kodun, her yerde match ile durumu elle incelemesi gerekmez.
Ama eksileri de vardır. Bir durumun hangi başka duruma geçeceğini yine başka
durumlar bilmek zorundadır; yani durumlar birbirine bir miktar bağlıdır. Örneğin
IncelemeBekleyen ile Yayinlanmis arasına Planlandi gibi yeni bir durum
eklerseniz, IncelemeBekleyen kodunu da değiştirmek gerekir.
Ayrıca biraz tekrar vardır. Gonderi üzerindeki bazı metotlar Option::take
ile durumu çıkarır, karşılık gelen durumsal metodu çağırır ve sonucu tekrar
alan içine koyar. Bu tekrar çok artarsa makro gibi başka araçlar düşünmek
isteyebilirsiniz.
Durumları ve Davranışı Türlerin Kendisi Olarak Kodlamak
Şimdi durum desenini Rust’a daha doğal gelen başka bir yaklaşımla yeniden düşünelim. Bu kez durumları trait nesneleriyle saklamak yerine, doğrudan farklı türler olarak kodlayacağız. Böylece Rust’ın tür denetimi, geçersiz durumları ve geçersiz geçişleri derleme zamanında yakalayabilecek.
18-11 numaralı listedeki kullanımın ilk kısmına tekrar bakalım:
use gunce::Gonderi;
fn main() {
let mut post = Gonderi::yeni();
post.metin_ekle("Bugun ogle yemeginde salata yedim");
assert_eq!("", post.icerik());
post.inceleme_iste();
assert_eq!("", post.icerik());
post.onayla();
assert_eq!("Bugun ogle yemeginde salata yedim", post.icerik());
}
Yeni yaklaşımda, gönderi taslakken icerik metodunun hiç olmamasını
sağlayacağız. Böylece taslak gönderinin içeriğini okumaya çalışan kod derleme
hatası alır. 18-19 numaralı liste, Gonderi ile TaslakGonderi türlerini
gösteriyor.
pub struct Gonderi {
icerik: String,
}
pub struct TaslakGonderi {
icerik: String,
}
impl Gonderi {
pub fn yeni() -> TaslakGonderi {
TaslakGonderi {
icerik: String::new(),
}
}
pub fn icerik(&self) -> &str {
&self.icerik
}
}
impl TaslakGonderi {
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
}
icerik metoduna sahip Gonderi ile icerik metodu olmayan TaslakGonderiHem Gonderi hem TaslakGonderi içinde gizli icerik alanı vardır. Ama artık
durum alanı yoktur; çünkü durumu struct türünün kendisi ifade eder. Gonderi
yayınlanmış gönderiyi temsil eder. Gonderi::yeni ise Gonderi değil,
TaslakGonderi döndürür. Böylece her gönderi taslak olarak başlamak zorunda
kalır.
TaslakGonderi üstünde metin_ekle vardır; ama icerik yoktur. Yani taslak
gönderinin içeriğini okumaya çalışan kod derlenmez.
Şimdi geçişleri de tür dönüşümleri olarak ifade edelim. TaslakGonderi
üzerindeki inceleme_iste, IncelemeBekleyenGonderi döndürsün; onun üstündeki
onayla da Gonderi döndürsün.
pub struct Gonderi {
icerik: String,
}
pub struct TaslakGonderi {
icerik: String,
}
impl Gonderi {
pub fn yeni() -> TaslakGonderi {
TaslakGonderi {
icerik: String::new(),
}
}
pub fn icerik(&self) -> &str {
&self.icerik
}
}
impl TaslakGonderi {
// --snip--
pub fn metin_ekle(&mut self, text: &str) {
self.icerik.push_str(text);
}
pub fn inceleme_iste(self) -> IncelemeBekleyenGonderi {
IncelemeBekleyenGonderi {
icerik: self.icerik,
}
}
}
pub struct IncelemeBekleyenGonderi {
icerik: String,
}
impl IncelemeBekleyenGonderi {
pub fn onayla(self) -> Gonderi {
Gonderi {
icerik: self.icerik,
}
}
}
TaslakGonderi için inceleme_iste, IncelemeBekleyenGonderi için onayla tanımlamakBu metotlar selfin sahipliğini alır; yani eski tür tüketilir ve yerine yeni
durumu temsil eden yeni tür üretilir. Böylece örneğin bir kez incelemeye
gönderdiğiniz taslağı eski haliyle kullanmayı sürdüremezsiniz.
Bu yeni tasarım nedeniyle main içindeki kullanım da biraz değişir. Her geçişte
yeni tür döndüğü için, sonucu tekrar aynı değişken adına bağlamamız gerekir.
use gunce::Gonderi;
fn main() {
let mut post = Gonderi::yeni();
post.metin_ekle("Bugun ogle yemeginde salata yedim");
let post = post.inceleme_iste();
let post = post.onayla();
assert_eq!("Bugun ogle yemeginde salata yedim", post.icerik());
}
mainBu sürüm artık klasik nesne yönelimli durum desenini birebir izlemiyor; çünkü
durum geçişleri tamamen Gonderi içinde saklı değil. Ama önemli bir kazanç
sağlıyor: geçersiz durumlar tür sistemi yüzünden artık mümkün değil. Yani
yayınlanmamış gönderinin içeriğini göstermeye çalışan hata türü, üretime
çıkmadan önce derleme sırasında yakalanır.
Özet
Bu bölümün sonunda Rust’ı nesne yönelimli sayıp saymamanızdan bağımsız olarak, Rust’ta trait nesneleriyle bazı nesne yönelimli özellikleri elde edebileceğinizi görmüş oldunuz. Dinamik dağıtım biraz çalışma zamanı maliyeti getirir; ama buna karşılık daha esnek yapılar kurabilirsiniz.
Ayrıca Rust, nesne yönelimli dillerde bulunmayan sahiplik gibi güçlü araçlara da sahiptir. Bu yüzden nesne yönelimli desenler Rust’ta her zaman en iyi çözüm olmayabilir. Yine de gerektiğinde kullanabileceğiniz geçerli bir araç olarak ellerinizin altındadır.
Sıradaki bölümde desenlere bakacağız. Kitap boyunca onlara birkaç kez değindik; ama şimdiye kadar tüm güçlerini görmedik. Şimdi buna geçelim.