domingo, 12 de julio de 2020

Rust es orientado a objeto?


Antes que nada vamos a aclarar que que un lenguaje sea orientado a objeto ni es bueno, ni es malo. Yo en lo personal opino que es mejor que un lenguaje no sea orientado a objeto, que lo soporte parcialmente. 

Rust está influenciado por muchos paradigmas de programación, incluyendo POO; por ejemplo, con características de la programación funcional. Podría decirse que los lenguajes POO comparten ciertas características comunes, a saber, objetos, encapsulación y herencia.

El libro Design Patterns: Elements of Reusable Object-Oriented Software de Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides (Addison-Wesley Professional, 1994) coloquialmente conocido como el libro The Gang of Four, es un catálogo de libros orientados a objetos patrones de diseño. Y define POO de esta manera: Los programas orientados a objetos están formados por objetos. Un objeto empaqueta tanto los datos como los procedimientos que operan en esos datos. Los procedimientos generalmente se denominan métodos u operaciones.

Usando esta definición, Rust está orientado a objetos: las estructuras y las enumeraciones tienen datos, y los bloques implementados proporcionan métodos en estructuras y enumeraciones. Aunque las estructuras y enumeraciones con métodos no se denominan objetos, proporcionan la misma funcionalidad, según la definición de objetos de la Banda de los Cuatro.

Otro aspecto comúnmente asociado con POO es la idea de encapsulamiento, lo que significa que los detalles de implementación de un objeto no son accesibles para el código que usa ese objeto. Por lo tanto, la única forma de interactuar con un objeto es a través de su API pública o interfaz; el código que usa el objeto no debería poder alcanzar las partes internas del objeto y cambiar los datos o el comportamiento directamente. Esto permite que el programador cambie y refactorice las partes internas de un objeto sin necesidad de cambiar el código que usa el objeto.

En Rust podemos usar 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 una estructura AveragedCollection que tiene un campo que contiene un vector de valores i32. La estructura también puede tener un campo que contenga el promedio de los valores en el vector:

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

La estructura está marcada como pub para que otro código pueda usarla, pero los campos dentro de la estructura permanecen privados. Esto es importante en este caso porque queremos asegurarnos de que siempre que se agregue o elimine un valor de la lista, el promedio también se actualice. Hacemos esto implementando métodos de adición, eliminación y promedio en la estructura:

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;
    }
}

Los métodos públicos add, remove y average son las únicas formas de acceder o modificar datos en una instancia de AveragedCollection. Cuando se agrega un elemento a la lista usando el método add o se elimina usando el método remove, las implementaciones de cada llamada llaman al método privado update_average que también maneja la actualización del campo promedio.

Dejamos la lista y los campos promedio privados para que no haya forma de que el código externo agregue o elimine elementos al campo de la lista directamente; de lo contrario, el campo promedio podría no estar sincronizado cuando cambie la lista. El método promedio devuelve el valor en el campo promedio, permitiendo que el código externo lea el promedio pero no lo modifique.

Debido a que hemos encapsulado los detalles de implementación de la estructura AveragedCollection, podemos cambiar fácilmente aspectos, como la estructura de datos, en el futuro. Por ejemplo, podríamos usar un HashSet <i32> en lugar de un Vec <i32> para el campo de lista. Mientras las firmas de los metodos agregar, eliminar y los métodos públicos promedio permanezcan igual, no será necesario cambiar el código que usa AveragedCollection. Si hiciéramos pública la lista, este no sería necesariamente el caso: HashSet <i32> y Vec <i32> tienen diferentes métodos para agregar y eliminar elementos, por lo que el código externo probablemente tendría que cambiar si modificara la lista directamente.

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

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

Si un lenguaje debe tener herencia para ser un lenguaje orientado a objetos, entonces Rust no es uno. No hay forma de definir una estructura que herede los campos de la estructura principal y las implementaciones de métodos. Sin embargo, si está acostumbrado a tener herencia en su caja de herramientas de programación, puede usar otras soluciones en Rust, dependiendo de su razón para buscar la herencia en primer lugar.

Eliges la herencia por dos razones principales: 
  • Una es para la reutilización del código: puede implementar un comportamiento particular para un tipo, y la herencia le permite reutilizar esa implementación para un tipo diferente. En su lugar, puede compartir el código Rust utilizando implementaciones de métodos predeterminados de trait. 
  • La otra razón para usar la herencia se relaciona con el sistema de tipos: para permitir que un tipo secundario se use en los mismos lugares que el tipo primario. Esto también se llama polimorfismo, lo que significa que puede sustituir varios objetos entre sí en tiempo de ejecución si comparten ciertas características.

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 funcionar con datos de múltiples tipos. Para la herencia, esos tipos son generalmente subclases.

En su lugar, Rust usa genéricos para abstraer sobre diferentes tipos posibles y límites de rasgos para imponer restricciones sobre lo que esos tipos deben proporcionar. Esto a veces se llama polimorfismo paramétrico acotado.

La herencia ha caído recientemente en desgracia como una 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 principal, sino que 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 que una subclase herede de una clase, lo que restringirá aún más la flexibilidad del diseño de un programa.

Por estas razones, Rust adopta un enfoque diferente, utilizando objetos de trait en lugar de herencia. 

Dejo link: 
https://doc.rust-lang.org/book/ch17-01-what-is-oo.html

No hay comentarios.:

Publicar un comentario