Capítulo 8: Refinamiento paso a paso


En gran medida, la programación es la ciencia de resolver problemas por computadora. Debido a que los problemas suelen ser difíciles, las soluciones y los programas que implementan esas soluciones también pueden ser difíciles. Para facilitar el desarrollo de esas soluciones, debe adoptar una metodología y disciplina que reduzca el nivel de esa complejidad a una escala manejable.

En los primeros años de la programación, el concepto de informática como ciencia era más o menos un experimento de ilusiones. Nadie sabía mucho sobre programación en aquellos días y pocos la consideraban una disciplina de ingeniería en el sentido convencional. Sin embargo, a medida que la programación maduró, comenzó a surgir una disciplina de este tipo. La piedra angular de esa disciplina es el entendimiento de que la programación se realiza en un entorno social en el que los programadores deben trabajar juntos. Si ingresa a la industria, es casi seguro que será uno de los muchos programadores que trabajan para desarrollar un programa grande. Además, es casi seguro que ese programa main y requerirá main tenencia main más allá de su aplicación originalmente prevista. Alguien querrá que el programa incluya alguna característica nueva o funcione de alguna manera diferente. Cuando eso ocurre, un nuevo equipo de programadores debe entrar y realizar los cambios necesarios en los programas. Si los programas están escritos en un estilo individual con poca o ninguna similitud, lograr que todos trabajen juntos de manera productiva es extremadamente difícil.

Para combatir este problema, los programadores comenzaron a desarrollar un conjunto de metodologías de programación que se denominan colectivamente. Ingeniería de software . El uso de buenas habilidades de ingeniería de software no solo facilita la lectura y comprensión de sus programas por parte de otros programadores, sino que también facilita la escritura de dichos programas en primer lugar. Uno de los avances metodológicos más importantes de la ingeniería de software es la estrategia de diseño de arriba hacia abajo o refinamiento por etapas , que consiste en resolver problemas partiendo del problema en su conjunto. Se divide todo el problema en pedazos y luego se resuelve cada uno, dividiéndolos aún más si es necesario. Esta estrategia de arriba hacia abajo se complementa con prueba iterativa donde se asegura de que las piezas más pequeñas de la solución estén funcionando antes de continuar.

Un ejercicio de refinamiento escalonado.

Para ilustrar el concepto de refinamiento paso a paso, enseñemos Karel a resolver un nuevo problema. Imagina que Karel ahora vive en un mundo que se parece a esto:

En cada una de las columnas, hay una torre de conos de altura desconocida, aunque algunas columnas (como la 7ª y la 9ª en el mundo de muestra) pueden estar vacías. El trabajo de Karel es recolectar todos los conos en cada una de estas torres, volver a colocarlos en la esquina más al este de la primera fila y luego regresar a su posición inicial. Por lo tanto, cuando Karel termine su trabajo en el ejemplo anterior, los 25 conos actualmente en las torres deben apilarse en la esquina de la novena columna y la primera fila, de la siguiente manera:

Es importante destacar que puede suponer que Karel inicialmenteempiezacon cero conos en su bolsa. Cada cono recogido se agrega a su bolsa. Al poner conos en la esquina, Karel puede usar el conos_en_bolsa() prueba. También podemos suponer que las columnas no llegan hasta el muro más al norte.

La clave para resolver este problema es descomponer el programa de la manera correcta, sin dejar de poder probar sobre la marcha. Esta tarea es más compleja que las otras que ha visto, lo que hace que elegir los subproblemas adecuados sea más importante para obtener una solución exitosa.

El principio del diseño de arriba hacia abajo.

La idea clave en el refinamiento gradual es que debe comenzar el diseño de su programa desde arriba, que se refiere al nivel del programa que es conceptualmente más alto y más abstracto. En este nivel, el problema de la torre cono se divide claramente en tres fases independientes. Primero, Karel tiene que recolectar todos los conos . En segundo lugar, Karel tiene que depositarlos en la última intersección. En tercer lugar, Karel tiene que volver a su posición Karel . Esta descomposición conceptual del problema sugiere que la main() La función para este programa tendrá la siguiente estructura:

   def main():
      recoger_todo_conos()
      soltar_todo_conos()
      volver_a_casa()

En este nivel, el problema es fácil de entender. Por supuesto, quedan algunos detalles en forma de funciones que aún no ha escrito. Aun así, es importante mirar cada nivel de la descomposición y convencerse de que, siempre que crea que las funciones que está a punto de escribir resolverán los subproblemas correctamente, entonces tendrá una solución al problema en su conjunto. .

Pruebas iterativas sobre la marcha

Ahora que ha definido la estructura del programa como un todo, es el momento de moverse en el primer subproblema, que consiste en recopilar todos los conos . Esta tarea es en sí misma más complicada que los simples problemas de los capítulos anteriores. Recoger todo el conos significa que tienes que recoger el conos en cada torre hasta llegar a la última esquina. El hecho de que necesite repetir una operación para cada torre sugiere que necesita un while ciclo aquí. los while ciclo repetirá el proceso de recoger_una_torre() y luego moviéndose.

Precaución: Es peligroso tratar de escribir todo el programa sin pruebas a medida que avanza Si comete un error será difícil encontrar el error. Sabemos que vamos a repetir el proceso de recogida de una torre. Escribamos y prueba recogiendo una sola torre antes de que pongamos el recoger_una_torre() proceso en un ciclo for . Asítemporalmentepodemos comenzar con la siguiente definición de recoger_todo_conos() :

   def recoger_todo_conos() :
      # implementación temporal con fines de prueba
      recoger_una_torre()
      moverse()

Como principio rector, si tiene un ciclo complejo, pruebe elcuerpodel ciclo antes de escribir el ciclo completo.

Torre de recogida de refinamiento

Cuando recoger_una_torre() se llama, Karel está parado en la base de una torre de conos o está parado en una esquina vacía. En el primer caso, debe recoger el conos en la torre. En este último, simplemente puede moverse en. Esta situación parece una aplicación para if declaración, en la que escribirías algo como esto:

   if conos_presente():
      recoger_la_torre_real()

Antes de agregar dicha declaración al código, debe pensar si necesita realizar esta prueba. A menudo, los programas pueden simplificarse mucho observando que los casos que al principio parecen especiales pueden tratarse exactamente de la misma manera que la situación más general. En el problema actual, ¿qué sucede si decide que hay una torre conos en cada avenida, pero que algunas de esas torres tienen una altura de conos cero? Hacer uso de esta información simplifica el programa porque ya no tiene que probar si hay una torre en una avenida en particular.

los recoger_una_torre() La función es todavía lo suficientemente compleja como para que exista un nivel adicional de descomposición. Para recopilar todos los cono s en una torre, Karel debe realizar los siguientes pasos:

  1. Gire a la izquierda para enfrentar el conos en la torre.
  2. Recoge todos los conos de la torre y conos cuando no encuentres más conos .
  3. Date la vuelta para mirar hacia la parte inferior del mundo.
  4. Regresa a la pared que representa el suelo.
  5. Gire a la izquierda para estar listo en moverse hasta la siguiente esquina.

Una vez más, este esquema proporciona un modelo para recoger_una_torre() función, que se ve así:

   defrecoger_una_torre():
      girar_izquierda()
      recoger_línea_de_conos()
      media_vuelta()
      moverse_a_la_pared()
      girar_izquierda()

Condiciones previas y posteriores a la función

los girar_izquierda() comandos al principio y al final del recoger_una_torre() ambas funciones son fundamentales para la corrección de este programa. Cuando recoger_una_torre() se llama, Karel siempre está en algún lugar de la 1ª fila mirando al este. Cuando finalice su funcionamiento, el programa en su conjunto funcionará correctamente solo si Karel vuelve a mirar hacia el este en esa misma esquina. Las condiciones que deben ser verdaderas antes de que se llame a una función se denominan condiciones previas ; Las condiciones que deben aplicarse una vez finalizada la función se conocen como postcondiciones .

Cuando defina una función, tendrá menos problemas si escribe exactamente cuáles son las condiciones previas y posteriores. Una vez que lo haya hecho, debe asegurarse de que el código que escribe siempre deje satisfechas las condiciones posteriores, asumiendo que las condiciones previas se cumplieron para empezar. Por ejemplo, piense en lo que sucede si llama recoger_una_torre() cuando Karel está en la 1ª fila mirando al este. El primero girar_izquierda() La función deja Karel mirando al norte, lo que significa que Karel está correctamente alineado con la columna de conos representa la torre. los recoger_línea_de_conos() función, que aún no se ha escrito pero que, sin embargo, realiza una tarea que usted comprende conceptualmente, simplemente moverse s sin girar. Así, al final de la llamada a recoger_línea_de_conos() , Karel seguirá mirando al norte. los media_vuelta() Llame por tanto sale Karel orientación sur. Me gusta recoger_línea_de_conos() , el moverse_a_la_pared() La función no implica ningún giro, sino simplemente moverse s hasta que golpea la pared límite. Dado que Karel está orientado al sur, este muro límite será el que se encuentra en la parte inferior de la pantalla, justo debajo de la primera fila. El final girar_izquierda() Por lo tanto, el comando deja Karel en la 1ª fila mirando al este, lo que satisface la Karel .

Repitiendo el proceso

Usted run su programa y borra con éxito una torre y deja Karel en la Karel prometida. ¡Wahoo! ¡Acaba de alcanzar un hito en la resolución de esta difícil tarea! Ahora tenemos que repetir el proceso de limpiar una torre usando un while ciclo .

Pero que hace esto while ciclo parece? Primero que nada, debes pensar en la prueba condicional. Desea que Karel detenga cuando golpee la pared al final de la fila. Por lo tanto, desea que Karel continúe mientras el espacio en frente despejado . Por lo tanto, sabes que el recoger_todo_conos() La función incluirá un while ciclo que utiliza el frente_despejado() prueba. En cada posición, desea que Karel recolecte todos los conos en la torre que comienza en esa esquina. Si le da un nombre a esa operación, que podría ser algo como recoger_una_torre() , puede seguir adelante y escribir una definición para recoger_todo_conos() función aunque aún no haya completado los detalles.

Sin embargo, debes tener cuidado. El código para recoger_todo_conos() no se ve así:

   defrecoger_todo_conos():
      # buggy ciclo !
      while frente_despejado():
         recoger_una_torre()
         moverse()

Esta implementación tiene errores exactamente por la misma razón que la primera versión de la ConoLínea El programa del capítulo 6 falló en hacer su trabajo. Hay un error de poste de cerca en esta versión del código, porque Karel necesita probar la presencia de una torre cono en la última avenida. La implementación correcta es:

   defrecoger_todo_conos():
      while frente_despejado():
         recoger_una_torre()
         moverse()
      recoger_una_torre()

Tenga en cuenta que esta función tiene exactamente la misma estructura que el programa main programa Place Cono Line presentado en el capítulo 6. La única diferencia es que este programa llama recoger_una_torre() donde el otro llamó poner_cono() . Estos dos programas son cada uno ejemplos de una estrategia general que se ve así:

   defrecoger_todo_conos():
      while frente_despejado():
          realizar alguna operacion
         moverse()
       Realiza la misma operación para la esquina final.

Puede usar esta estrategia siempre que necesite realizar una operación en cada esquina mientras moverse un camino que termina en una pared. Si recuerda la estructura general de esta estrategia, puede usarla siempre que encuentre un problema que requiera dicha operación. Las estrategias reutilizables de este tipo surgen con frecuencia en la programación y se denominan lenguajes de programación o patrones . Cuantos más patrones sepa, más fácil le resultará encontrar uno que se adapte a un tipo particular de problema.

Terminando

Aunque se ha realizado un arduo trabajo, todavía quedan varios cabos sueltos que deben resolverse. El programa main llama a dos funciones: soltar_todo_conos() y volver_a_casa() - que aún no están escritos. Similar, recoger_una_torre() llamadas recoger_línea_de_conos() y moverse_a_la_pared() . Afortunadamente, estas cuatro funciones son lo suficientemente simples para codificar sin más descomposición, especialmente si usa moverse_a_la_pared() en la definición de volver_a_casa() . Aquí está la implementación completa:


Siguiente capítulo