Charakterystyka Języków Zorientowanych Obiektowo
W społeczności programistycznej nie ma zgody co do tego, jakie cechy musi posiadać język, aby był uważany za zorientowany obiektowo. Rust jest pod wpływem wielu paradygmatów programowania, w tym OOP; na przykład, zbadaliśmy cechy pochodzące z programowania funkcyjnego w Rozdziale 13. Prawdopodobnie, języki OOP dzielą pewne wspólne cechy – a mianowicie obiekty, hermetyzację i dziedziczenie. Przyjrzyjmy się, co oznacza każda z tych cech i czy Rust je obsługuje.
Obiekty Zawierają Dane i Zachowanie
Książka Wzorce projektowe: Elementy reużywalnego oprogramowania obiektowego Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa (Addison-Wesley, 1994), potocznie nazywana książką Gang of Four, jest katalogiem obiektowych wzorców projektowych. Definiuje OOP w ten sposób:
Programy zorientowane obiektowo składają się z obiektów. Obiekt pakuje zarówno dane, jak i procedury, które operują na tych danych. Procedury są zazwyczaj nazywane metodami lub operacjami.
Zgodnie z tą definicją, Rust jest zorientowany obiektowo: struktury i wyliczenia posiadają dane, a bloki impl dostarczają metody dla struktur i wyliczeń. Mimo że struktur i wyliczeń z metodami nie nazywa się obiektami, zapewniają one tę samą funkcjonalność, zgodnie z definicją obiektów według Gang of Four.
Hermetyzacja Ukrywająca Szczegóły Implementacji
Innym aspektem powszechnie kojarzonym z OOP jest idea hermetyzacji, co oznacza, że szczegóły implementacji obiektu nie są dostępne dla kodu korzystającego z tego obiektu. Dlatego jedynym sposobem interakcji z obiektem jest jego publiczne API; kod używający obiektu nie powinien być w stanie bezpośrednio zmieniać wewnętrznych danych ani zachowań obiektu. Umożliwia to programiście zmianę i refaktoryzację wewnętrznych elementów obiektu bez konieczności zmiany kodu, który go używa.
Omówiliśmy, jak kontrolować hermetyzację w Rozdziale 7: Możemy użyć słowa kluczowego pub, aby zdecydować, które moduły, typy, funkcje i metody w naszym kodzie powinny być publiczne, a domyślnie wszystko inne jest prywatne. Na przykład, możemy zdefiniować strukturę AveragedCollection, która ma pole zawierające wektor wartości i32. Struktura może również mieć pole zawierające średnią wartości w wektorze, co oznacza, że średnia nie musi być obliczana na żądanie za każdym razem, gdy ktoś jej potrzebuje. Innymi słowy, AveragedCollection będzie dla nas buforować obliczoną średnią. Lista 18-1 zawiera definicję struktury AveragedCollection.
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
AveragedCollection, która przechowuje listę liczb całkowitych i średnią elementów w kolekcjiStruktura jest oznaczona jako pub, aby inny kod mógł jej używać, ale pola wewnątrz struktury pozostają prywatne. Jest to ważne w tym przypadku, ponieważ chcemy zapewnić, że za każdym razem, gdy wartość jest dodawana lub usuwana z listy, średnia jest również aktualizowana. Robimy to, implementując metody add, remove i average dla struktury, jak pokazano na Liście 18-2.
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
add, remove i average w AveragedCollectionPubliczne metody add, remove i average to jedyne sposoby dostępu lub modyfikacji danych w instancji AveragedCollection. Gdy element jest dodawany do list za pomocą metody add lub usuwany za pomocą metody remove, implementacje obu metod wywołują prywatną metodę update_average, która zajmuje się również aktualizacją pola average.
Pozostawiamy pola list i average prywatne, aby zewnętrzny kod nie mógł bezpośrednio dodawać ani usuwać elementów z pola list; w przeciwnym razie pole average mogłoby stać się niespójne, gdy list się zmienia. Metoda average zwraca wartość z pola average, umożliwiając zewnętrznemu kodowi odczyt average, ale nie jego modyfikację.
Ponieważ hermetyzowaliśmy szczegóły implementacji struktury AveragedCollection, możemy w przyszłości łatwo zmieniać aspekty, takie jak struktura danych. Na przykład, moglibyśmy użyć HashSet<i32> zamiast Vec<i32> dla pola list. Dopóki sygnatury publicznych metod add, remove i average pozostałyby takie same, kod używający AveragedCollection nie wymagałby zmian. Gdybyśmy uczynili list publicznym, niekoniecznie tak by było: HashSet<i32> i Vec<i32> mają różne metody dodawania i usuwania elementów, więc kod zewnętrzny prawdopodobnie musiałby się zmienić, gdyby modyfikował list bezpośrednio.
Jeśli hermetyzacja jest wymaganym aspektem, aby język był uważany za obiektowy, to Rust spełnia to wymaganie. Opcja użycia pub lub nie dla różnych części kodu umożliwia hermetyzację szczegółów implementacji.
Dziedziczenie jako System Typów i jako Udostępnianie Kodu
Dziedziczenie to mechanizm, dzięki któremu obiekt może dziedziczyć elementy z definicji innego obiektu, uzyskując w ten sposób dane i zachowanie obiektu-rodzica bez konieczności ponownego ich definiowania.
Jeśli język musi posiadać dziedziczenie, aby być obiektowym, to Rust nie jest takim językiem. Nie ma sposobu, aby zdefiniować strukturę, która dziedziczy pola i implementacje metod struktury-rodzica bez użycia makra.
Jednakże, jeśli jesteś przyzwyczajony do posiadania dziedziczenia w swoim zestawie narzędzi programistycznych, możesz użyć innych rozwiązań w Rust, w zależności od powodu, dla którego pierwotnie sięgnąłeś po dziedziczenie.
Dziedziczenie wybrałbyś z dwóch głównych powodów. Jeden to ponowne wykorzystanie kodu: możesz zaimplementować określone zachowanie dla jednego typu, a dziedziczenie umożliwia ponowne wykorzystanie tej implementacji dla innego typu. Możesz to zrobić w ograniczony sposób w kodzie Rust, używając domyślnych implementacji metod cech, co widziałeś na Liście 10-14, gdy dodaliśmy domyślną implementację metody summarize do cechy Summary. Każdy typ implementujący cechę Summary miałby dostępną metodę summarize bez dodatkowego kodu. Jest to podobne do klasy nadrzędnej posiadającej implementację metody i dziedziczącej klasy podrzędnej również posiadającej implementację metody. Możemy również nadpisać domyślną implementację metody summarize podczas implementowania cechy Summary, co jest podobne do klasy podrzędnej nadpisującej implementację metody odziedziczonej z klasy nadrzędnej.
Drugi powód użycia dziedziczenia dotyczy systemu typów: aby umożliwić użycie typu potomnego w tych samych miejscach co typ nadrzędny. Nazywa się to również polimorfizmem, co oznacza, że można podstawiać wiele obiektów jeden za drugi w czasie wykonania, jeśli dzielą one pewne cechy.
Polimorfizm
Dla wielu osób polimorfizm jest synonimem dziedziczenia. Ale w rzeczywistości jest to bardziej ogólna koncepcja, która odnosi się do kodu, który może pracować z danymi wielu typów. Dla dziedziczenia te typy są zazwyczaj podklasami.
Rust zamiast tego używa generyków do abstrakcji nad różnymi możliwymi typami i ograniczeń cech do narzucania ograniczeń na to, co te typy muszą zapewniać. Jest to czasami nazywane ograniczonym polimorfizmem parametrycznym.
Rust wybrał inny zestaw kompromisów, nie oferując dziedziczenia. Dziedziczenie często grozi współdzieleniem większej ilości kodu niż to konieczne. Podklasy nie zawsze powinny dzielić wszystkie cechy swojej klasy nadrzędnej, ale będą to robić w przypadku dziedziczenia. Może to sprawić, że projekt programu będzie mniej elastyczny. Wprowadza to również możliwość wywoływania metod w podklasach, które nie mają sensu lub powodują błędy, ponieważ metody nie mają zastosowania do podklasy. Ponadto, niektóre języki zezwalają tylko na pojedyncze dziedziczenie (co oznacza, że podklasa może dziedziczyć tylko z jednej klasy), co dodatkowo ogranicza elastyczność projektu programu.
Z tych powodów Rust przyjmuje inne podejście, używając obiektów cech zamiast dziedziczenia, aby osiągnąć polimorfizm w czasie wykonania. Przyjrzyjmy się, jak działają obiekty cech.