Nesneye Yönelik Tasarım Modeli Uygulamak
Durum kalıbı, nesne yönelimli bir tasarım kalıbıdır. Desenin özü, bir değerin dahili olarak sahip olabileceği bir dizi durum tanımlamamızdır. Durumlar bir dizi durum nesnesi ile temsil edilir ve değerin davranışı durumuna bağlı olarak değişir. "draft", "review" veya "published" kümesinden bir durum nesnesi olacak şekilde durumunu tutmak için bir alana sahip olan bir blog yazısı yapısı örneği üzerinde çalışacağız.
Durum nesneleri işlevselliği paylaşır: Rust'ta elbette nesneler ve kalıtım yerine yapıları ve özellikleri kullanırız. Her durum nesnesi kendi davranışından ve ne zaman başka bir duruma geçmesi gerektiğini yönetmekten sorumludur. Bir durum nesnesini tutan değer, durumların farklı davranışları veya durumlar arasında ne zaman geçiş yapılacağı hakkında hiçbir şey bilmez.
Durum kalıbını kullanmanın avantajı, programın iş gereksinimleri değiştiğinde, durumu tutan değerin kodunu veya değeri kullanan kodu değiştirmemize gerek kalmamasıdır. Kurallarını değiştirmek ya da belki daha fazla durum nesnesi eklemek için yalnızca durum nesnelerinden birinin içindeki kodu güncellememiz gerekecektir. Durum modelini kullanarak bir blog yazısı iş akışını aşamalı olarak uygulamaya başlayalım.
Nihai işlevsellik şu şekilde görünecektir:
The final functionality will look like this:
- Bir blog gönderisi boş bir taslak olarak başlar.
- Taslak yapıldığında, gönderinin gözden geçirilmesi istenir.
- Gönderi onaylandığında yayınlanır.
- Yalnızca yayınlanan blog gönderileri, içeriği yazdırılacak şekilde döndürür, bu nedenle onaylanmamış gönderiler yanlışlıkla yayınlanamaz.
Bir gönderide yapılmaya çalışılan diğer değişikliklerin hiçbir etkisi olmamalıdır. Örneğin, inceleme talep etmeden önce taslak bir blog gönderisini onaylamaya çalışırsak, gönderi yayınlanmamış bir taslak olarak kalmalıdır.
Liste 17-11 bu iş akışını kod biçiminde göstermektedir: bu, blog
adlı bir kütüphane kasasına uygulayacağımız API'nin örnek bir kullanımıdır.
Bu henüz derlenmeyecektir çünkü blog
crate'ini henüz yazmadık.
Dosya adı: src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Kullanıcının Post::new
ile yeni bir taslak blog yazısı oluşturmasına izin vermek istiyoruz. Blog yazısına metin eklenmesine izin vermek
istiyoruz. Onaydan önce yazının içeriğini hemen almaya çalışırsak, yazı hala taslak olduğu için herhangi bir metin almamalıyız.
Gösterim amacıyla koda assert_eq!
ekledik. Bunun için mükemmel bir birim testi, taslak bir blog gönderisinin içerik yönteminden
boş bir dize döndürdüğünü iddia etmek olurdu, ancak bu örnek için test yazmayacağız.
Daha sonra, yazının incelenmesi için bir isteği etkinleştirmek istiyoruz ve inceleme beklenirken içeriğin boş bir dize döndürmesini istiyoruz. Gönderi onay aldığında yayınlanmalıdır, yani content çağrıldığında gönderi metni döndürülecektir.
Kasadan etkileşimde bulunduğumuz tek türün Post
türü olduğuna dikkat edin. Bu tür durum kalıbını kullanacak ve bir gönderinin
taslak halinde, inceleme için bekliyor veya yayınlanmış olabileceği çeşitli durumları temsil eden üç durum nesnesinden biri olacak bir
değer tutacaktır. Bir durumdan diğerine geçiş Post
türü içinde dahili olarak yönetilecektir. Durumlar, kütüphanemizin kullanıcıları
tarafından Post
tanımı üzerinde çağrılan metodlara yanıt olarak değişir, ancak durum değişikliklerini doğrudan yönetmeleri gerekmez.
Ayrıca kullanıcılar, bir gönderiyi incelenmeden önce yayınlamak gibi durumlarla ilgili bir hata yapamazlar.
Post'u Tanımlama ve Taslak Durumunda Yeni Bir Örnek Oluşturma
Kütüphanenin yazılmasına başlayalım! Bazı içerikleri tutan genel bir Post
yapısına ihtiyacımız olduğunu biliyoruz,
bu nedenle yapının tanımı ve bir Post
tanımı oluşturmak için ilişkili bir genel new
fonksiyonu ile başlayacağız,
Liste 17-12'de gösterildiği gibi. Ayrıca bir Post için
tüm durum nesnelerinin sahip olması gereken davranışı tanımlayacak özel bir
State
tanımı oluşturacağız.
Ardından Post
, durum nesnesini tutmak için state
adlı özel bir alanda bir Option<T>
içinde Box<dyn State>
'in trait
nesnesini
tutacaktır. Option<T>
'nin neden gerekli olduğunu birazdan göreceksiniz.
Dosya adı: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
State
özelliği, farklı posta durumları tarafından paylaşılan davranışı tanımlar. State
nesneleri Draft
, PendingReview
ve Published
'dir
ve hepsi State
tanımını uygular. Şimdilik, tanımın herhangi bir metodu yoktur ve sadece Draft
durumunu tanımlayarak başlayacağız
çünkü bir gönderinin başlamasını istediğimiz durum budur.
Yeni bir Post
oluşturduğumuzda, state
alanını bir Box
tutan Some
değerine ayarlarız. Bu Box
, Draft
yapısının yeni bir
örneğine işaret eder. Bu, yeni bir Post
örneği oluşturduğumuzda, taslak olarak başlamasını sağlar. Post
'un durum alanı özel olduğu için,
başka bir durumda bir Post
oluşturmanın hiçbir yolu yoktur! Post::new
fonksiyonunda, content
alanını yeni, boş bir String
olarak ayarlarız.
Gönderi İçeriği Metnini Saklama
Liste 17-11'de add_text
adlı bir metodu çağırabilmek ve ona blog yazısının metin içeriği olarak eklenecek bir &str
iletebilmek istediğimizi
gördük. Bunu, içerik alanını pub
olarak göstermek yerine bir metod olarak uyguluyoruz, böylece daha sonra içerik alanının
verilerinin nasıl okunacağını kontrol edecek bir metod uygulayabiliriz. add_text
metodu oldukça basittir, bu nedenle Liste 17-13'teki
uygulamayı impl Post
bloğuna ekleyelim:
Dosya adı: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
trait State {}
struct Draft {}
impl State for Draft {}
add_text
metodu self
öğesine değişebilir bir referans alır, çünkü add_text
öğesini çağırdığımız Post
tanımını değiştiriyoruz.
Daha sonra content
'teki String
üzerinde push_str
'yi çağırıyoruz ve kaydedilen içeriğe eklemek için metin argümanını iletiyoruz.
Bu davranış, gönderinin içinde bulunduğu duruma bağlı değildir, bu nedenle durum modelinin bir parçası değildir.
add_text
yöntemi state
alanıyla hiç etkileşime girmez, ancak desteklemek istediğimiz davranışın bir parçasıdır.
Taslak Gönderinin İçeriğinin Boş Olmasını Sağlama
add_text
öğesini çağırdıktan ve gönderimize bir miktar içerik ekledikten sonra bile, Liste 17-11'in 7. satırında gösterildiği gibi,
gönderi hala taslak durumunda olduğu için content
metodunun boş bir dizgi dilimi döndürmesini istiyoruz. Şimdilik, content
metodunu bu gereksinimi karşılayacak en basit şeyle uygulayalım: her zaman boş bir dizgi dilimi döndürmek. Bunu daha sonra bir gönderinin
durumunu değiştirip yayınlanabilmesini sağladığımızda değiştireceğiz. Şimdiye kadar, yazılar yalnızca taslak durumunda olabilir,
bu nedenle yazı içeriği her zaman boş olmalıdır. Liste 17-14 bu yer tutucu uygulamasını göstermektedir:
Dosya adı: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
}
trait State {}
struct Draft {}
impl State for Draft {}
Bu eklenen content
metoduyla, Liste 17-11'den 7. satıra kadar her şey amaçlandığı gibi çalışır.
Gönderinin Durumu Değişikliklerinin Gözden Geçirilmesini Talep Etme
Ardından, durumunu Draft
'tan PendingReview
olarak değiştirmesi gereken bir gönderinin gözden geçirilmesini istemek
için işlevsellik eklememiz gerekiyor. Liste 17-15 bu kodu gösterir:
Dosya adı: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
Post
'a, self
öğesine değişken bir referans alacak request_review
adında bir genel metod veriyoruz.
Ardından Post
'un mevcut durumu üzerinde dahili bir request_review
metodunu çağırıyoruz ve bu ikinci request_review
metodu
mevcut durumu tüketip yeni bir durum döndürüyor.
request_review
metodunu State
tanımını ekliyoruz; tanımı uygulayan tüm türlerin artık request_review
metodunu uygulaması gerekecektir.
Metodun ilk parametresinin self
, &self
veya &mut self
yerine self
olduğuna dikkat edin: Box<Self>
. Bu söz dizimi,
yöntemin yalnızca türü taşıyan bir Box
üzerinde çağrıldığında geçerli olduğu anlamına gelir. Bu söz dizimi Box<Self>
'in sahipliğini
alarak eski durumu geçersiz kılar, böylece Post
'un durum değeri yeni bir duruma dönüşebilir.
Eski durumu kullanmak için request_review
yönteminin durum değerinin sahipliğini alması gerekir. Post
'un state
alanındaki
Option
burada devreye girer: take
metodunu çağırarak Some
değerini state
alanından çıkarırız ve yerine None
değerini bırakırız,
çünkü Rust yapılarda doldurulmamış alanlar olmasına izin vermez. Bu, state
değerini ödünç almak yerine Post
'un dışına taşımamızı sağlar.
Daha sonra post
'un state
değerini bu işlemin sonucuna ayarlayacağız.
State
değerinin sahipliğini almak için self.state = self.state.request_review();
gibi bir kodla doğrudan ayarlamak yerine
state
değerini geçici olarak None
olarak ayarlamamız gerekir. Bu, Post
'un biz onu yeni bir duruma dönüştürdükten sonra
eski durum değerini kullanamamasını sağlar.
Draft
üzerindeki request_review
yöntemi, bir gönderinin inceleme için beklediği durumu temsil eden yeni bir PendingReview
yapısının yeni,
Box
'un bir örneğini döndürür. PendingReview
struct
'ı da request_review
yöntemini uygular ancak herhangi bir dönüştürme yapmaz.
Bunun yerine, kendisini döndürür, çünkü zaten PendingReview
durumunda olan bir gönderi için inceleme istediğimizde, gönderi PendingReview
durumunda kalmalıdır.
Şimdi state
modelinin avantajlarını görmeye başlayabiliriz: Post
üzerindeki request_review
yöntemi, state
değeri ne olursa olsun aynıdır.
Her state
kendi kurallarından sorumludur.
Post
üzerindeki content
metodunu olduğu gibi bırakacağız ve boş bir dizgi dilimi döndüreceğiz. Artık hem PendingReview
durumunda hem de
Draft
durumunda bir Post
'a sahip olabiliriz, ancak PendingReview
durumunda aynı davranışı istiyoruz.
Liste 17-11 artık 10. satıra kadar çalışıyor!
content
'in Davranışını Değiştirmek için approve
Ekleme
approve
metodu request_review
metoduna benzer olacaktır: state
'i, mevcut state
'in onaylandığında sahip olması
gerektiğini söylediği değere ayarlayacaktır, Liste 17-16'da gösterildiği gibi:
Dosya adı: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
State
tanımına approve
metodunu ekliyoruz ve Published
durum olan State
'i uygulayan yeni bir struct
ekliyoruz.
PendingReview
üzerinde request_review
metodunun çalışmasına benzer şekilde, bir Draft
üzerinde approve
yöntemini çağırırsak,
approve
self
değerini döndüreceği için hiçbir etkisi olmayacaktır. PendingReview
üzerinde approve
yöntemini çağırdığımızda,
Published
yapısının yeni, Box
'un bir tanımını döndürür. Published
struct
'ı, State
tanımını uygular ve
hem request_review
yöntemi hem de approve
yöntemi için kendini döndürür, çünkü bu durumlarda gönderi Published
durumunda kalmalıdır.
Şimdi Post
üzerindeki content
metodunu güncellememiz gerekiyor. Content
'ten döndürülen değerin Post
'un mevcut durumuna
bağlı olmasını istiyoruz, bu nedenle Post
'un Liste 17-17'de gösterildiği gibi durumuna göre tanımlanmış bir content
metoduna
temsilci göndermesini sağlayacağız:
Dosya adı: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
Amaç tüm bu kuralları State
'i uygulayan yapıların içinde tutmak olduğundan, state
'teki değer üzerinde bir content
yöntemi
çağırıyoruz ve post
örneğini (yani self
'i) bir argüman olarak geçiriyoruz. Daha sonra state
değeri üzerinde content
metodunu
kullanarak döndürülen değeri döndürüyoruz.
Option
üzerinde as_ref
yöntemini çağırıyoruz çünkü değerin sahibi olmak yerine Option
içindeki değere bir referans istiyoruz.
state
bir Option<Box<dyn State>>
olduğu için, as_ref
yöntemini çağırdığımızda bir Option<&Box<dyn State>>
döndürülür.
Eğer as_ref
'i çağırmasaydık, state
'i fonksiyon parametresinin ödünç alınan &self
'inin dışına taşıyamayacağımız için bir hata alırdık.
Daha sonra unwrap
metodunu çağırıyoruz, ki bu metodun asla panik yaratmayacağını biliyoruz, çünkü Post
üzerindeki metodların,
bu metodlar tamamlandığında state
'in her zaman Some
değeri içereceğini garanti ettiğini biliyoruz.
Bu, Bölüm 9'un “Derleyiciden Daha Fazla Bilgiye Sahip Olduğunuz Durumlar” kısmında bahsettiğimiz,
derleyici bunu anlayamasa da None değerinin asla mümkün olmadığını bildiğimiz durumlardan biridir.
Bu noktada, &Box<dyn State>
üzerinde content
'i çağırdığımızda, deref zorlaması &
ve Box
üzerinde etkili olacak,
böylece content
yöntemi sonuçta State
tanımını uygulayan tür üzerinde çağrılacaktır.
Bu, State
özellik tanımına içerik eklememiz gerektiği anlamına gelir ve Liste 17-18'de gösterildiği gibi, hangi duruma sahip
olduğumuza bağlı olarak hangi içeriğin döndürüleceğine ilişkin mantığı buraya koyacağız:
Dosya adı: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
content
yöntemi için boş bir dizgi dilimi döndüren varsayılan bir uygulama ekliyoruz.
Bu, Draft
ve PendingReview
yapılarında içerik uygulamamıza gerek olmadığı anlamına gelir. Published
struct
'ı,
content
yöntemini geçersiz kılacak ve post.content
içindeki değeri döndürecektir.
Bölüm 10'da tartıştığımız gibi, bu yöntem üzerinde yaşam süresi ek açıklamalarına ihtiyacımız olduğunu unutmayın. Argüman olarak bir gönderiye referans alıyoruz ve bu gönderinin bir kısmına referans döndürüyoruz, bu nedenle döndürülen referansın yaşam süresi post argümanının yaşam süresiyle ilişkilidir.
Ve işimiz bitti - Liste 17-11'in tamamı artık çalışıyor! State
modelini blog yazısı iş akışı kurallarıyla uyguladık.
Kurallarla ilgili mantık, Post
'un içine dağılmak yerine state
nesnelerinde yaşıyor.
Durum Kalıbının Ödünleşimleri
Rust'ın, bir gönderinin her bir durumda sahip olması gereken farklı davranış türlerini kapsüllemek için nesne
yönelimli durum modelini uygulayabildiğini gösterdik. Post üzerindeki yöntemler çeşitli davranışlar hakkında hiçbir şey bilmiyor.
Kodu düzenlediğimiz şekilde, yayınlanan bir gönderinin farklı davranış biçimlerini öğrenmek için tek bir yere bakmamız gerekiyor:
Published
yapısındaki State
özelliğinin uygulanması.
State
kalıbını kullanmayan alternatif bir uygulama oluşturacak olsaydık, bunun yerine Post
üzerindeki yöntemlerde veya hatta
gönderinin durumunu kontrol eden ve bu yerlerde davranışı değiştiren ana kodda eşleşme ifadeleri kullanabilirdik.
Bu, bir gönderinin yayınlanmış durumda olmasının tüm sonuçlarını anlamak için birkaç yere bakmamız gerektiği anlamına gelirdi!
Bu, ne kadar çok durum eklersek o kadar artacaktır: bu match
ifadelerinin her biri başka bir kola ihtiyaç duyacaktır.
State
kalıbı ile Post
metodları ve Post
'u kullandığımız yerler eşleşme ifadelerine ihtiyaç duymaz ve yeni bir
state
eklemek için sadece yeni bir struct
eklememiz ve trait
metodlarını bu struct
üzerinde uygulamamız gerekir.
State
kalıbını kullanan uygulamanın daha fazla işlevsellik eklemek için genişletilmesi kolaydır.
State
kalıbını kullanan kodu korumanın basitliğini görmek için bu önerilerden birkaçını deneyin:
- Gönderinin durumunu PendingReview'den Draft'a değiştiren bir reddetme yöntemi ekleyin.
State
Published
olarak değiştirilmeden önce onaylamak için iki çağrı yapılmasını zorunlu kılın.- Kullanıcıların yalnızca bir gönderi
Draft
durumundayken metin içeriği eklemesine izin verin. İpucu:state
nesnesinin içerikle ilgili değişebilecek şeylerden sorumlu olmasını ancakPost
'u değiştirmekten sorumlu olmamasını sağlayın. State
modelinin bir dezavantajı, durumlar arasındaki geçişleri durumlar gerçekleştirdiği için bazı durumların birbirine bağlı olmasıdır.PendingReview
ilePublished
arasınaScheduled
gibi başka birstate
eklersek,PendingReview
'daki koduScheduled
'a geçiş yapacak şekilde değiştirmemiz gerekir.PendingReview
'in yeni bir durum eklendiğinde değişmesi gerekmeseydi daha az iş olurdu, ancak bu başka bir tasarım modeline geçmek anlamına gelir.
Diğer bir dezavantajı ise bazı mantıkları tekrarlamış olmamızdır. Yinelemenin bir kısmını ortadan kaldırmak için,
State
özelliğindeki request_review
ve approve
yöntemleri için self
döndüren varsayılan uygulamalar yapmayı deneyebiliriz;
ancak bu, nesne güvenliğini ihlal eder, çünkü özellik somut self
'in tam olarak ne olacağını bilmez. State
'i bir özellik nesnesi olarak
kullanabilmek istiyoruz, bu nedenle yöntemlerinin nesne güvenli olmasına ihtiyacımız var.
Diğer tekrarlar, Post
üzerindeki request_review
ve approve
yöntemlerinin benzer uygulamalarını içerir.
Her iki yöntem de Option
'ın state alanındaki değer üzerinde aynı yöntemin uygulanmasına delege eder ve state
alanının yeni
değerini sonuca ayarlar. Post
üzerinde bu kalıbı izleyen çok sayıda yöntemimiz olsaydı, tekrarı ortadan kaldırmak için bir makro
tanımlamayı düşünebilirdik (Bölüm 19'daki "Makrolar" bölümüne bakın).
State
kalıbını tam olarak nesne yönelimli diller için tanımlandığı gibi uygulayarak, Rust'ın güçlü yanlarından olabildiğince
yararlanamıyoruz. Geçersiz durumları ve geçişleri derleme zamanı hatalarına dönüştürebilecek blog
kasasında yapabileceğimiz
bazı değişikliklere bakalım.
Durumları ve Davranışları Tür Olarak Kodlama
Farklı bir dizi ödünleşim elde etmek için durum modelini nasıl yeniden düşüneceğinizi göstereceğiz. Durumları ve geçişleri tamamen kapsüllemek yerine, dış kodun bunlar hakkında hiçbir bilgiye sahip olmaması için durumları farklı türlere kodlayacağız. Sonuç olarak, Rust'ın tür kontrol sistemi, yalnızca yayınlanmış gönderilere izin verilen taslak gönderileri kullanma girişimlerini bir derleyici hatası vererek önleyecektir.
Liste 17-11'deki main
'in ilk bölümünü ele alalım:
Dosya adı: src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Post::new
kullanarak taslak durumunda yeni gönderilerin oluşturulmasını ve gönderinin içeriğine metin ekleme özelliğini hala etkinleştiriyoruz.
Ancak taslak bir gönderide boş bir dize döndüren bir content
yöntemine sahip olmak yerine, taslak gönderilerin içerik yöntemine
hiç sahip olmamasını sağlayacağız. Bu şekilde, bir taslak gönderinin içeriğini almaya çalışırsak, bize yöntemin mevcut
olmadığını söyleyen bir derleyici hatası alırız. Sonuç olarak, taslak gönderi içeriğini üretimde yanlışlıkla görüntülememiz imkansız olacaktır,
çünkü bu kod derlenmeyecektir bile. Liste 17-19, bir Post
yapısının ve bir DraftPost
yapısının tanımının yanı sıra her birindeki
yöntemleri gösterir:
Dosya adı: src/lib.rs
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Hem Post
hem de DraftPost
yapıları, blog yazısı metnini saklayan özel bir içerik alanına sahiptir.
Yapılar artık state
alanına sahip değildir çünkü state
kodlamasını yapıların türlerine taşıyoruz.
Post
yapısı yayınlanmış bir gönderiyi temsil edecektir ve içeriği döndüren bir içerik yöntemine sahiptir.
Hala bir Post::new
fonksiyonumuz var, ancak Post
'un bir örneğini döndürmek yerine DraftPost
'un bir örneğini döndürüyor. content
gizli olduğundan ve Post
döndüren herhangi bir fonksiyon bulunmadığından, şu anda bir Post
tanımı oluşturmak mümkün değildir.
DraftPost
yapısının bir add_text
yöntemi vardır, bu nedenle daha önce olduğu gibi içeriğe metin ekleyebiliriz, ancak
DraftPost
'un tanımlanmış bir content
yöntemi olmadığını unutmayın! Böylece program tüm gönderilerin taslak gönderiler olarak
başlamasını sağlar ve taslak gönderilerin içerikleri görüntülenemez. Bu kısıtlamaları aşmaya yönelik herhangi bir girişim derleyici hatasıyla sonuçlanacaktır.
Geçişleri Farklı Türlere Dönüşümler Olarak Uygulama
Peki yayınlanmış bir gönderiyi nasıl alacağız? Bir taslak gönderinin yayınlanmadan önce gözden geçirilmesi ve
onaylanması gerektiği kuralını uygulamak istiyoruz. Bekleyen inceleme durumundaki bir gönderi hala herhangi bir içerik
göstermemelidir. Bu kısıtlamaları, PendingReviewPost
adında başka bir yapı ekleyerek, bir PendingReviewPost
döndürmek için
DraftPost
üzerinde request_review
yöntemini tanımlayarak ve bir Post
döndürmek için PendingReviewPost
üzerinde bir approve
yöntemi
tanımlayarak, Liste 17-20'de gösterildiği gibi yazalım:
Geçişleri Farklı Türlere Dönüştürme Olarak Uygulama
Peki yayınlanmış bir gönderiyi nasıl alacağız? Bir taslak gönderinin yayınlanmadan önce gözden geçirilmesi ve
onaylanması gerektiği kuralını uygulamak istiyoruz. Bekleyen inceleme durumundaki bir gönderi hala herhangi bir içerik göstermemelidir.
Bu kısıtlamaları, PendingReviewPost
adında başka bir yapı ekleyerek, bir PendingReviewPost
döndürmek için DraftPost
üzerinde
request_review
yöntemini tanımlayarak ve bir Post
döndürmek için PendingReviewPost
üzerinde bir approve
yöntemi tanımlayarak,
Liste 17-20'de gösterildiği gibi uygulayalım:
Dosya adı: src/lib.rs
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
request_review
ve approve
yöntemleri self
'in sahipliğini alır, böylece DraftPost
ve PendingReviewPost
örneklerini
tüketir ve bunları sırasıyla bir PendingReviewPost
'a ve yayınlanmış bir Post
'a dönüştürür. Bu şekilde, üzerlerinde request_review
çağrısı
yaptıktan sonra kalan DraftPost
örneklerimiz olmayacaktır. PendingReviewPost
yapısının üzerinde tanımlanmış bir content
yöntemi yoktur,
bu nedenle içeriğini okumaya çalışmak DraftPost
'ta olduğu gibi bir derleyici hatasıyla sonuçlanır. Tanımlanmış bir content
yöntemi olan
yayınlanmış bir Post
örneği almanın tek yolu bir PendingReviewPost
üzerinde approve
yöntemini çağırmak olduğundan ve bir
PendingReviewPost
almanın tek yolu bir DraftPost
üzerinde request_review
yöntemini çağırmak olduğundan, artık blog yazısı
iş akışını tür sistemine kodladık.
Ancak main
'de de bazı küçük değişiklikler yapmamız gerekiyor. request_review
ve approve
yöntemleri, çağrıldıkları yapıyı
değiştirmek yerine yeni örnekler döndürür, bu nedenle döndürülen örnekleri kaydetmek için daha fazla let post =
gölgeleme ataması eklememiz
gerekir. Ayrıca, taslak ve bekleyen inceleme gönderilerinin içerikleriyle ilgili iddiaların boş dizgiler olmasını sağlayamayız ve
bunlara ihtiyacımız da yok: artık bu durumlardaki gönderilerin içeriğini kullanmaya çalışan kodu derleyemeyiz.
main
'deki güncellenmiş kod Liste 17-21'de gösterilmektedir:
Dosya adı: src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Post
'u yeniden atamak için main
'de yapmamız gereken değişiklikler, bu uygulamanın artık nesne yönelimli durum modeline
tam olarak uymadığı anlamına geliyor: durumlar arasındaki dönüşümler artık tamamen Post
uygulaması içinde kapsüllenmiyor.
Bununla birlikte, kazancımız, tür sistemi ve derleme zamanında gerçekleşen tür denetimi nedeniyle geçersiz durumların artık
imkansız olmasıdır! Bu, yayınlanmamış bir gönderinin içeriğinin görüntülenmesi gibi belirli hataların üretime geçmeden önce
keşfedilmesini sağlar.
Kodun bu versiyonunun tasarımı hakkında ne düşündüğünüzü görmek için bu bölümün başında önerilen görevleri Liste 17-21'den sonra olduğu gibi blog sandığı üzerinde deneyin. Bazı görevlerin bu tasarımda zaten tamamlanmış olabileceğini unutmayın.
Rust'ın nesne yönelimli tasarım kalıplarını uygulama yeteneğine sahip olmasına rağmen, durumu tür sistemine kodlamak gibi diğer kalıpların da Rust'ta mevcut olduğunu gördük. Bu kalıpların farklı ödünleşimleri vardır. Nesne yönelimli kalıplara çok aşina olsanız da, Rust'ın özelliklerinden yararlanmak için sorunu yeniden düşünmek, derleme zamanında bazı hataları önlemek gibi faydalar sağlayabilir. Nesne yönelimli kalıplar, nesne yönelimli dillerin sahip olmadığı sahiplik gibi bazı özellikler nedeniyle Rust'ta her zaman en iyi çözüm olmayacaktır.
Özet
Bu bölümü okuduktan sonra Rust'ın nesne yönelimli bir dil olduğunu düşünseniz de düşünmeseniz de, artık Rust'ta bazı nesne
yönelimli özellikler elde etmek için trait
nesnelerini kullanabileceğinizi biliyorsunuz. Dinamik gönderim, kodunuza biraz çalışma
zamanı performansı karşılığında biraz esneklik sağlayabilir. Bu esnekliği, kodunuzun sürdürülebilirliğine yardımcı olabilecek nesne yönelimli
kalıpları uygulamak için kullanabilirsiniz. Rust ayrıca sahiplik gibi nesne yönelimli dillerin sahip olmadığı başka özelliklere de sahiptir.
Nesne yönelimli bir kalıp, Rust'ın güçlü yönlerinden yararlanmanın her zaman en iyi yolu olmayacaktır, ancak kullanılabilir bir seçenektir.
Daha sonra, Rust'ın çok fazla esneklik sağlayan bir başka özelliği olan kalıplara bakacağız. Kitap boyunca bunlara kısaca baktık ancak henüz tam kapasitelerini görmedik. Hadi başlayalım!