Abstracción

Objetivos: Explicar qué es el concepto de abstracción en computación y como ayuda a resolver problemas complejos.

Temas:


El concepto de abstracción

La complejidad de un procedimiento es aproximadamente proporcional a su tamaño (número de líneas) multiplicado por el número de variables que se declaran en él. Esto significa que la complejidad de un procedimiento puede crecer rápidamente hasta niveles inmanejables por una persona. A modo de ejemplo, una solución para la tarea 4 de este curso consiste en un procedimiento de unas 50 líneas con 14 variables. Un procedimiento con el doble de la líneas y el doble de variables sería 4 veces más complejo.

De hecho, pocas personas son capaces de manejar la complejidad de un procedimiento de más de 200 líneas. Esto se traduce en que a pesar de lograr escribir el procedimiento, nunca se llega a hacerlo a andar correctamente (no se logra eliminar todos los errores de programación) ¿Significa esto que existe un cota para el tamaño de los programas que puede escribir una persona? Afortunadamente no. En la práctica esta cota existe para el tamaño de los procedimientos, pero no los programas. Se pueden escribir programas de millones de líneas, en base a funciones o procedimientos de 20 a 50 líneas.

La clave para poder escribir programas complejos es descomponer el problema a resolver en subproblemas más pequeños y luego resolverlos en forma independiente. Esto significa que hay que elegir uno de estos subproblemas y programar una solución de él abstrayéndose completamente de los detalles sobre como programar una solución para el resto de los subproblemas. Luego, se elige otro subproblema y se resuelve abstrayéndose de los detalles de implementación del primer subproblema u otros subproblemas aún no resueltos. Así, hasta resolver todos los subproblemas.

Diseño:

El proceso de descomposición de un problema en partes más simples se denomina diseño. El diseño comienza con un problema que puede ser complejo de resolver. Su resultado es un conjunto de subproblemas que constituyen las partes del problema. Estas partes se pueden resolver independientemente unas de otras.

Abstracción:

La abstracción es una estrategia de resolución de problemas en la cual el programador se concentra en resolver una parte del problema ignorando completamente los detalles sobre cómo se resuelven el resto de las partes. En este proceso de abstracción se considera que el resto de las partes ya han sido resueltas y por lo tanto pueden servir de apoyo para resolver la parte que recibe la atención.

Una vez hecha la descomposición en subproblemas, resulta natural resolver cada uno de ellos en una función o un procedimiento. Como los subproblemas son más sencillos que el problema original, su complejidad (dada por su tamaño y número de variables) será inferior a la complejidad del mismo problema resuelto por medio de un solo procedimiento.

La abstracción es la estrategia de programación más importante en computación. Sin abstracción las personas serían incapaces de abordar los problemas complejos. La pericia de un programador no está en ser veloz para escribir líneas de programa, si no que en saber descubrir, en el proceso de diseño, cuáles son las partes del problema, y luego resolver cada una de ellas abstrayéndose de las otras.

Un ejemplo de abstracción es el hecho de que uno pueda conducir un automóvil sin ser un mecánico (lo cual probablemente no era cierto con los primeros vehículos). Al conducir, uno se abstrae de cómo funciona la combustión en el motor. Sólo se requiere saber cómo se maneja el volante y los pedales, y cuales son las reglas del tránsito.

Ejemplo:

Supongamos que se necesita un programa que calcule los impuestos que deben pagar un grupo de personas. El impuesto varía de acuerdo al sueldo que percibe cada persona. Para ello se establecen varios tramos de sueldo. Estos tramos se encuentran en un archivo de nombre impuestos.txt. Su contenido podría ser por ejemplo:

     10000    30
      2000    20
       500    10
         0     0
El primer campo (columnas 0 a 9 ) es el monto de sueldo y el segundo (columnas 10 a 15) es la tasa de impuestos que debe pagar una persona por cada peso que perciba por sobre ese sueldo. Es así como una persona que gane 5000, percibe 3000 pesos por sobre el tramo 2000 y por lo tanto debe pagar el 20% de 3000. Además percibe 1500 pesos por sobre los 500 por lo que debe pagar el 10% de 1500. Por lo tanto esa persona debe pagar 750 pesos en impuestos.

Solución:

Este problema lo podemos resolver con un solo procedimiento, pero el programa resultante será menos complejo si descomponemos el problema en 2 subproblemas y luego resolvemos ambos subproblemas en forma independiente.

1er. problema: dados un arreglo con los límites de sueldo de cada tramo, un arreglo que contiene las tasas de impuesto que corresponde pagar en cada tramo y un sueldo, calcular el monto a pagar en impuestos por ese sueldo.

2do. problema: construir dos arreglos con los valores contenidos en el archivo impuestos.txt y luego establecer un diálogo para pedir sucesivamente varios sueldos y responder cuanto se debe pagar por cada uno de ellos, suponiendo que existe una función que calcula el impuesto a partir de los dos arreglos y el sueldo.

La idea es resolver cada uno de estos problemas en una función o un procedimiento independiente. El primero se puede resolver definiendo una función impuestos que reciba como parámetros los dos arreglos y el sueldo, y entregue como resultado el impuesto a pagar. Esta función tendrá la siguiente forma:

    int impuestos(Array tramos, Array tasas, int sueldo) {
      ... calcular el impuesto en función de tramos, tasas y sueldo ...
      return el impuesto calculado
    }
Al resolver este subproblema, el programador debe abstraerse de que los tramos están en un archivo y que hay que realizar un diálogo con el usuario, porque estos detalles son parte del 2do problema. Tampoco debe preocuparse de cómo se construyen los arreglos. Ellos son parámetros del problema.

El segundo problema es resuelto en el procedimiento run(). En él se lee el archivo, se construyen los arreglos, se dialoga con el usuario y se invoca la función impuestos. La forma del código será entonces:

    void run() {
      - tramos= new Array(...);
      - tasas= new Array(...);
      - Inicializar ambos arreglos con los valores leídos del archivo
        impuestos.txt
      - Hacer un ciclo de diálogo con el usuario
      - Por cada sueldo ingresado por el usuario
        se invoca la función impuestos:
        monto= impuestos(tramos, tasas, sueldo);
      - desplegar monto.
    }
Al resolver este subproblema, el programador se abstrae acerca del detalle de cómo se calcula el impuesto, porque esto es parte del primer subproblema.

El beneficio que trae la abstracción consiste en que resolver estos dos subproblemas en forma independiente resulta menos complejo que resolver el problema original considerando todos sus detalles en forma simultánea.

Conclusión: resulta conveniente descomponer un problema en subproblemas simples de resolver, porque la suma de las complejidades de las partes es menor que la complejidad del todo. Mientras más grande es el programa final, mayor será la diferencia en complejidad.

En el ejemplo anterior, cada uno de los subproblemas se resolvió por medio de una función o procedimiento. Esta es la forma de resolver los problemas en los lenguajes tradicionales (Pascal, C, Visual Basic y otros). En los lenguajes orientados a objetos (Java, C++, Smalltalk y otros) también existe la posibilidad de resolver cada uno de los subproblemas en clases.


Problemas tipo

Aprender a descomponer convenientemente un problema en sus partes toma bastante tiempo. No elegir adecuadamente las partes puede originar un problema que es aún más difícil que el original. Por esta razón, en este curso en la mayoría de los problemas la descomposición de los problemas está dada en el enunciado.

Los típicos problemas que Ud. verá en los controles, tareas y ejercicios serán:

Más adelante se preguntarán problemas de definición de clase y problemas completos acerca de clases.