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());
}
Liste 17-11: blog kasamızda olmasını istediğimiz davranışı gösteren kod
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 {}
Liste 17-12: Yeni bir Post örneği, bir State tanımı ve bir Draft yapısı oluşturan Post yapısının ve
new fonksiyonunun tanımı
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 {}
Liste 17-13: Bir gönderinin content'ine metin eklemek için add_text metodunu
süreklemek
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 {}
Liste 17-14: Post'a content metodu için her zaman boş bir dizgi dilimi
döndüren bir yer tutucu süreklemesi ekleme
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
}
}
Liste 17-15: Post ve State tanımı üzerinde request_review metodlarının
yazılması
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
}
}
Liste 17-16: Post ve State tanımı üzerinde approve metodunu uygulama
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
}
}
Liste 17-17: State'de bir content metoduna yetki vermek için
Post üzerindeki content metodunu güncelleme
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
}
}
Liste 17-18: State tanımına content yöntemini ekleme
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.
StatePublishedolarak 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
Draftdurumundayken metin içeriği eklemesine izin verin. İpucu:statenesnesinin içerikle ilgili değişebilecek şeylerden sorumlu olmasını ancakPost'u değiştirmekten sorumlu olmamasını sağlayın. Statemodelinin bir dezavantajı, durumlar arasındaki geçişleri durumlar gerçekleştirdiği için bazı durumların birbirine bağlı olmasıdır.PendingReviewilePublishedarasınaScheduledgibi başka birstateeklersek,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);
}
}
Liste 17-19: content yöntemine sahip bir Post ve content
metodu olmayan bir DraftPost
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,
}
}
}
Liste 17-20: DraftPost'ta request_review çağrılarak oluşturulan bir
PendingReviewPost ve PendingReviewPost'u yayınlanmış bir Post'a dönüştüren bir approve yöntemi
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());
}
Liste 17-21: Blog gönderisi iş akışının yeni süreklemesinin kullanmak için
main'deki değişiklikler
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!