Инкапсуляция, скрывающая детали реализации
Ещё одним аспектом, обычно связанным с ООП, является понятие инкапсуляции, которое означает, что детали реализации объекта недоступны для кода, использующего этот объект. Поэтому единственным способом взаимодействия с объектом является через его публичный API; код, использующий объект, не должен иметь доступа к внутренним частям объекта и напрямую изменять данные или поведение. Это позволяет программисту изменять и рефакторить внутренние части объекта, не меняя при этом код, использующий объект.
Мы обсуждали, как контролировать инкапсуляцию в главе 7: мы можем использовать ключевое слово pub, чтобы определить, какие модули, типы, функции и методы в нашем коде должны быть публичными, и по умолчанию все остальное является приватным. Например, мы можем определить структуру AveragedCollection, которая имеет поле, содержащее вектор значений типа i32. Структура также может иметь поле, содержащее среднее значение элементов в векторе, что означает, что среднее значение не нужно вычислять при каждом запросе. Другими словами, AveragedCollection будет кешировать вычисленное среднее значение для нас. В листинге 17-1 приведено определение структуры AveragedCollection.
Filename: src/lib.rs
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
Листинг 17-1: Структура AveragedCollection, которая хранит список целых чисел и среднее значение элементов в коллекции
Структура помечена pub, чтобы другой код мог использовать её, но поля внутри структуры остаются приватными. Это важно в этом случае, потому что мы хотим гарантировать, что при добавлении или удалении значения из списка среднее значение также обновляется. Мы это делаем, реализуя методы add, remove и average на структуре, как показано в листинге 17-2.
Filename: src/lib.rs
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;
}
}
Листинг 17-2: Реализации публичных методов add, remove и average на AveragedCollection
Публичные методы add, remove и average — единственный способ доступа к или модификации данных в экземпляре AveragedCollection. Когда элемент добавляется в list с использованием метода add или удаляется с использованием метода remove, реализации каждого вызывают приватный метод update_average, который также обрабатывает обновление поля average.
Мы оставляем поля list и average приватными, чтобы внешний код не имел возможности напрямую добавлять или удалять элементы из поля list; в противном случае поле average может быть устаревшим при изменении list. Метод average возвращает значение из поля average, позволяя внешнему коду читать average, но не модифицировать его.
Поскольку мы инкапсулировали детали реализации структуры AveragedCollection, мы можем легко изменить аспекты, такие как структура данных, в будущем. Например, мы могли бы использовать HashSet<i32> вместо Vec<i32> для поля list. Пока сигнатуры публичных методов add, remove и average остаются такими же, код, использующий AveragedCollection, не потребуется изменять. Если мы сделаем list публичным, это не обязательно будет так: HashSet<i32> и Vec<i32> имеют разные методы для добавления и удаления элементов, поэтому внешний код, вероятно, должен был бы измениться, если он напрямую модифицировал list.
Если инкапсуляция является обязательным аспектом для того, чтобы язык считался объектно-ориентированным, то Rust соответствует этому требованию. Возможность использовать pub или нет для разных частей кода позволяет инкапсулировать детали реализации.