Características de los lenguajes orientados a objetos

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Características de los lenguajes orientados a objetos. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, exploraremos las características de los lenguajes orientados a objetos, incluyendo objetos, encapsulación e herencia, y examinaremos si Rust admite estas características.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/floating_types("Floating-point Types") rust/DataTypesGroup -.-> rust/type_casting("Type Conversion and Casting") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/integer_types -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/floating_types -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/type_casting -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/function_syntax -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/expressions_statements -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/method_syntax -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/traits -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} rust/operator_overloading -.-> lab-100441{{"Características de los lenguajes orientados a objetos"}} end

Características de los lenguajes orientados a objetos

No existe un consenso en la comunidad de programación sobre qué características debe tener un lenguaje para ser considerado orientado a objetos. Rust está influenciado por muchos paradigmas de programación, incluyendo la programación orientada a objetos (OOP); por ejemplo, exploramos las características que provienen de la programación funcional en el Capítulo 13. Puede decirse que los lenguajes OOP comparten ciertas características comunes, a saber, objetos, encapsulación y herencia. Veamos qué significa cada una de esas características y si Rust las admite.

Los objetos contienen datos y comportamiento

El libro Diseño de Patrones: Elementos de Software Orientado a Objetos Reutilizable de Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides (Addison-Wesley, 1994), conocido popularmente como el libro de Los Cuatro Grandes, es un catálogo de patrones de diseño orientados a objetos. Lo define de la siguiente manera:

Los programas orientados a objetos están compuestos por objetos. Un objeto agrupa tanto datos como los procedimientos que operan sobre esos datos. Los procedimientos se denominan generalmente métodos o operaciones.

Con esta definición, Rust es orientado a objetos: los structs y los enums tienen datos, y los bloques impl proporcionan métodos para los structs y los enums. Aunque los structs y los enums con métodos no se llaman objetos, proporcionan la misma funcionalidad, de acuerdo con la definición de objetos de Los Cuatro Grandes.

Encapsulación que oculta los detalles de implementación

Otro aspecto comúnmente asociado con la programación orientada a objetos es la idea de encapsulación, que significa que los detalles de implementación de un objeto no son accesibles para el código que utiliza ese objeto. Por lo tanto, la única manera de interactuar con un objeto es a través de su API pública; el código que utiliza el objeto no debería poder acceder a los internos del objeto y cambiar directamente los datos o el comportamiento. Esto permite al programador cambiar y refactorizar los internos de un objeto sin necesidad de cambiar el código que utiliza el objeto.

Discutimos cómo controlar la encapsulación en el Capítulo 7: podemos utilizar la palabra clave pub para decidir qué módulos, tipos, funciones y métodos en nuestro código deben ser públicos, y por defecto todo lo demás es privado. Por ejemplo, podemos definir un struct AveragedCollection que tiene un campo que contiene un vector de valores de i32. El struct también puede tener un campo que contiene el promedio de los valores en el vector, lo que significa que el promedio no tiene que ser calculado sobre demanda cada vez que alguien lo necesita. En otras palabras, AveragedCollection cacheará el promedio calculado para nosotros. La Lista 17-1 tiene la definición del struct AveragedCollection.

Nombre de archivo: src/lib.rs

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

Lista 17-1: Un struct AveragedCollection que mantiene una lista de enteros y el promedio de los elementos en la colección

El struct está marcado pub para que otro código pueda utilizarlo, pero los campos dentro del struct siguen siendo privados. Esto es importante en este caso porque queremos asegurarnos de que cada vez que se agrega o quita un valor de la lista, el promedio también se actualice. Hacemos esto implementando los métodos add, remove y average en el struct, como se muestra en la Lista 17-2.

Nombre de archivo: 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;
    }
}

Lista 17-2: Implementaciones de los métodos públicos add, remove y average en AveragedCollection

Los métodos públicos add, remove y average son las únicas maneras de acceder o modificar los datos en una instancia de AveragedCollection. Cuando se agrega un elemento a list utilizando el método add o se quita utilizando el método remove, las implementaciones de cada una llaman al método privado update_average que se encarga de actualizar el campo average también.

Dejamos los campos list y average privados para que no haya manera para el código externo de agregar o quitar elementos al campo list directamente; de lo contrario, el campo average podría quedar desincronizado cuando list cambia. El método average devuelve el valor en el campo average, permitiendo que el código externo lea el average pero no lo modifique.

Debido a que hemos encapsulado los detalles de implementación del struct AveragedCollection, podemos cambiar fácilmente aspectos, como la estructura de datos, en el futuro. Por ejemplo, podríamos utilizar un HashSet<i32> en lugar de un Vec<i32> para el campo list. Siempre y cuando las firmas de los métodos públicos add, remove y average se mantuvieran iguales, el código que utiliza AveragedCollection no tendría que cambiar. Si hiciéramos list público en su lugar, esto no necesariamente sería el caso: HashSet<i32> y Vec<i32> tienen métodos diferentes para agregar y quitar elementos, por lo que el código externo probablemente tendría que cambiar si estuviera modificando list directamente.

Si la encapsulación es un aspecto requerido para que un lenguaje sea considerado orientado a objetos, entonces Rust cumple con ese requisito. La opción de utilizar pub o no para diferentes partes del código permite la encapsulación de los detalles de implementación.

Herencia como sistema de tipos y como compartir código

La herencia es un mecanismo mediante el cual un objeto puede heredar elementos de la definición de otro objeto, obteniendo así los datos y el comportamiento del objeto padre sin tener que definirlos nuevamente.

Si un lenguaje debe tener herencia para ser orientado a objetos, entonces Rust no es ese tipo de lenguaje. No hay forma de definir un struct que herede los campos y las implementaciones de métodos del struct padre sin utilizar una macro.

Sin embargo, si estás acostumbrado a tener herencia en tu herramienta de programación, puedes utilizar otras soluciones en Rust, dependiendo de la razón por la que optaste por la herencia en primer lugar.

Elegirías herencia por dos razones principales. Una es para la reutilización de código: puedes implementar un comportamiento particular para un tipo, y la herencia te permite reutilizar esa implementación para un tipo diferente. Puedes hacer esto de manera limitada en el código de Rust utilizando implementaciones predeterminadas de métodos de trato, que viste en la Lista 10-14 cuando agregamos una implementación predeterminada del método summarize en el trato Summary. Cualquier tipo que implemente el trato Summary tendría el método summarize disponible sin necesidad de más código. Esto es similar a una clase padre que tiene una implementación de un método y una clase hija heredada que también tiene la implementación del método. También podemos anular la implementación predeterminada del método summarize cuando implementamos el trato Summary, lo que es similar a una clase hija anulando la implementación de un método heredado de una clase padre.

La otra razón para utilizar herencia está relacionada con el sistema de tipos: para permitir que un tipo hijo se utilice en los mismos lugares que el tipo padre. Esto también se llama polimorfismo, que significa que puedes sustituir múltiples objetos entre sí en tiempo de ejecución si comparten ciertas características.

Polimorfismo

Para muchas personas, el polimorfismo es sinónimo de herencia. Pero en realidad es un concepto más general que se refiere al código que puede trabajar con datos de múltiples tipos. Para la herencia, esos tipos son generalmente subclases.

Rust en cambio utiliza genéricos para abstraerse sobre diferentes tipos posibles y límites de trato para imponer restricciones sobre lo que esos tipos deben proporcionar. Esto a veces se llama polimorfismo paramétrico acotado.

La herencia ha caído en desgracia recientemente como solución de diseño de programación en muchos lenguajes de programación porque a menudo corre el riesgo de compartir más código del necesario. Las subclases no siempre deben compartir todas las características de su clase padre, pero lo harán con la herencia. Esto puede hacer que el diseño de un programa sea menos flexible. También introduce la posibilidad de llamar a métodos en subclases que no tienen sentido o que causan errores porque los métodos no se aplican a la subclase. Además, algunos lenguajes solo permitirán herencia simple (es decir, una subclase solo puede heredar de una clase), lo que restringe aún más la flexibilidad del diseño de un programa.

Por estas razones, Rust adopta el enfoque diferente de utilizar objetos de trato en lugar de herencia. Veamos cómo los objetos de trato permiten el polimorfismo en Rust.

Resumen

¡Felicitaciones! Has completado el laboratorio de Características de los lenguajes orientados a objetos. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.