Conceptos
Básicos en Ingeniería de Software II
Principios
de la Ingeniería Software
Abstracción
Es
siempre importante e incluso imprescindible en muchos casos dividir un
proyecto en partes de acuerdo a los aspectos y objetivos de este, como
estrategia para su resolución. Cuando nos enfrentamos a un problema
complejo, dividir el mismo en partes adecuadamente elegidas suele ser la
diferencia entre resolverlo o no. La Abstracción es el proceso mediante
el cual se identifican los aspectos importantes de un fenómeno ignorando
sus detalles. De esta forma el diseñador puede reducir un problema
a interacciones entre un grupo de entidades con comportamientos establecidos
para cumplir con los objetivos iniciales. Estas entidades son a su vez
nuevos problemas, aunque claro está de menores dimensiones que el
original. Algunos de ellos posiblemente puedan resolverse en forma directa
al tener una complejidad accesible al entendimiento humano, mientras que
otras quizás requieran su descomposición en nuevas abstracciones,
en un proceso iterativo mediante el que establecemos niveles de abstracción,
para concentrarnos en los aspectos del problema que nos interesan en un
momento particular. La abstracción como herramienta para hacer frente
a problemas complejos es usada constantemente no solo en la Ingeniería.
Para un microbiólogo una célula humana es una entidad viva
que realiza complejos procesos químicos, mientras que para un médico
es una unidad que interactúa con infinidad de otras para conformar
otro organismo más complejo. Ambos tienen visiones diferentes de
la misma entidad simplemente porque se mueven en distintos niveles de abstracción,
lo que no implica que uno tenga un conocimiento más profundo de
las células humanas; sencillamente ambos conocen los aspectos de
esta necesarios para cumplir su trabajo.
Los
lenguajes mismos de programación son abstracciones, en este caso
del hardware subyacente y le permiten al programador pensar en variables
en lugar de direcciones de memoria por ejemplo. Los seres humanos tienen
importantes carencias para manejar muchos detalles en forma simultánea,
sin embargo están bien preparados para entender abstracciones en
diversos niveles. Podemos pensar en la Abstracción como el mecanismo
fundamental de los que nos doto la naturaleza, para resolver problemas
complejos y por lo tanto una cualidad esencial en un lenguaje de programación
es que este permita definir distintos niveles de abstracción. Al
poco tiempo del nacimiento de la informática, se había vuelto
claro que los lenguajes debían proveer mecanismos de abstracción
y fue así que los lenguajes incorporaron tipos estructurados de
datos, funciones y procedimientos. Sin embargo los que hayan programado
alguna vez en Pascal por ejemplo, estarán de acuerdo que los refinamientos
sucesivos de funciones para resolver los problemas, no reflejan precisamente
nuestra idea de abstracción. En realidad de mucho mayor éxito
han sido otros lenguajes como C en su tiempo, que nos da mayor abstracción
a través de las dos siguientes herramientas o como C++ en la actualidad,
que nos lleva aún más lejos.
Modularidad
Un
sistema complejo puede ser dividido en piezas más simples llamadas
módulos. Cada módulo es una entidad que al ser elegida en
forma adecuada, nos permite marcar dos niveles de abstracción: uno
que concierne a los detalles de implementación de este y otro en
el que lo importante es su interacción con el resto de los módulos
del sistema. Para que los grados de abstracción finales sean adecuados,
los módulos deben tener alta cohesión interna y baja conexión
con el resto. Si existen familias de datos o funciones poco relacionadas
dentro de un módulo o sea si el módulo presenta baja cohesión,
es probable que el diseñador se distraiga intentando entender detalles
de funcionamiento innecesarios de una de las familias, cuando en realidad
su interés está en otra, atentando por ende contra la facultad
de abstracción del sujeto. En este caso probablemente la mejor decisión
sería crear un módulo por familia y resolver las relaciones
entre ellas mediante interacciones de los módulos resultantes. El
caso contrario es aquel en el que dentro de un grupo de módulos
existen intensas interacciones al punto que no puedo pensar en uno de ellos
como una entidad aislada del resto. La situación anterior conocida
como alta conexión, exige sin dudas las limitaciones del diseñador
obligándolo a considerar en forma simultánea más aspectos
de los que le interesa y por ende conspirando nuevamente contra su capacidad
de abstracción. Una correcta modularización ayuda a aislar
los errores más fácilmente y por lo tanto mejora la Reparabilidad
que a su vez contribuye a tener programas más Correctos y Confiables.
También mejora la Mantenibilidad si los cambios se limitan a los
módulos, pudiendo mejorar la Robustez del sistema. Por último
mejora la Eficiencia del proceso de producción no solo al facilitar
la resolución del sistema, sino también contribuyendo a la
paralelización de la producción en grupos de trabajo responsables
de módulos.
Un
buen procedimiento para determinar los módulos de un sistema consiste
en dividir este en un conjunto manejable de módulo de baja conexión,
y subdividir los módulos de baja cohesión en otros más
pequeños recursivamente, dicho en otras palabras seguir una estrategia
de Divide & Conquer manteniendo en cada paso baja conexión y
alta cohesión entre módulos. El lenguaje C debe parte de
su popularidad, a las virtudes para definir módulos en archivos
y a mecanismos de alto nivel para compilar automáticamente sistemas
de numerosos módulos. Existe la idea de módulo como archivo;
esto es falso y aunque bien es cierto que frecuentemente hay archivos con
módulos de un sistema, existen otras entidades con la misma propiedad
como las clases por ejemplo.
Ocultamiento
de la Información
En
el punto anterior hablamos de la modularización y como esta potencia
nuestras facultades de abstracción a la hora de resolver los problemas.
Sin embargo la modularización por si sola puede contribuir poco
a la abstracción, si no está complementada con el Ocultamiento
de la Información en los módulos. En efecto pensar en un
módulo como entidad abstracta, no tiene sentido si el uso de los
recursos de este implica conocerlo en detalle. Pensemos en un módulo
que maneja un puñado de consultas de un sistema en una base de datos;
por más que toda la funcionalidad del módulo se encuentre
en un entidad aislada (e.g. un archivo) será poca la abstracción
de este que podré lograr, si para leer un dato en particular tengo
que escribir una compleja consulta que involucra diversas tablas en una
base distribuida. Lo ideal sería que el módulo presentara
una interfaz de alto nivel, que se encargara de procesar el pedido al nivel
de abstracción en que se piensa, sin tener que tener constantemente
presente la implementación elegida. La posibilidad de crear puntos
de acceso de alto nivel a los módulos no es suficiente para cumplir
con el Ocultamiento de la Información. Es importante sobre todo
en proyectos que involucran a más de una persona, que el lenguaje
proporcione mecanismos de protección para evitar que por alguna
razón otros usuarios no usen la interfaz estándar del módulo.
Si esto pasara, la visión del módulo dependería desde
que otro módulo lo uso y por lo tanto perdería carácter
de entidad abstracta.
El
ocultamiento de la información de un módulo mejora sin duda
su Amigabilidad y en consecuencia la Eficiencia en el uso del mismo por
otra persona. También mejora la Legibilidad en términos de
arquitectura, contribuyendo a su vez a incrementan la Reparabilidad, Correctitud
y Confiabilidad. El lenguaje C proporciona directivas para establecer cuales
de las funciones y datos son visibles desde el exterior, pero solo en forma
desasociada. Es aquí donde comienzan a marcarse las diferencias
más importantes con su sucesor, el C++.
Tipos
abstractos de datos
Un
tipo abstracto de datos, es una estructura de datos asociada con sus operaciones
en un módulo que oculta los detalles de implementación de
todos los componentes. Por ejemplo el tipo "Lista Ordenada" es un una estructura
que almacena una secuencia de datos para los que existe la comparación,
o sea factible de ser ordenada y un conjunto de operaciones para su manejo
como ser: Menor, Insertar, Mayor, etc. siendo estas funciones la única
forma de alterar el estado de la lista. Tener tipos abstractos de datos
sería como llegar al ideal de Modularización y Ocultamiento
de Información. En forma similar podríamos hablar de los
tipos abstractos "Lista", "Cola", etc pero el tipo "Lista Ordenada" presenta
una particularidad, sus elementos deben ser a su vez Tipos Abstractos de
Datos para los que este definida la función MayorQue, aún
cuando los tipos almacenados puedan ser distintos. La propiedad anterior
conocida como Polimorfismo será vista recurrentemente durante la
documentación ya que es uno de los pilares en que se base nuestra
biblioteca. Formalmente el polimorfismo significa que si alguna entidad
envía un estímulo a otra, no necesita conocer en tipo de
la misma; la entidad que recibe el estímulo se encarga de interpretarlo.
El C++ nos acerca sustancialmente al concepto de tipo abstracto de datos,
relajando un poco el principio de Ocultamiento de Información y
limitando el Polimorfismo en aras de la herencia y el chequeo estricto
de tipos respectivamente.
Generalidad
El
principio de Generalidad consiste en atacar un problema más genérico
que el que tengo entre manos. Como estrategia a largo plazo para el re-uso
de código la Generalidad suele ser una buena inversión, mejorando
directamente la Evolutividad y Flexibilidad, lo que en orden mejora a su
vez la Mantenibilidad y la Robustez. Siempre es bueno en la medida que
no comprometa demasiado la Performance por ejemplo, o retrase demasiado
los tiempos de desarrollo en un proyecto de corta vida. Si el sistema tiene
que funcionar por un largo tiempo ( que es lo común ) el tiempo
invertido en Generalidad se recupera con creces y por lo tanto es un factor
vital para la Eficiencia del ciclo de producción.
La
generalidad en C++ está basada en tres potencialidades del lenguaje:
El Polimorfismo, la herencia y el uso de Templates.