Cómo aprender los fundamentos de la ingeniería de software – de una manera más interesante y menos dolorosa

Este artículo pretende ser una guía introductoria a los fundamentos de la ingeniería de software. Lo he escrito asumiendo que usted, querido lector, puede no saber mucho sobre los fundamentos del campo, por qué son importantes, y cuándo debería molestarse en aprenderlos. Entraremos en cada una de estas preguntas y terminaremos discutiendo algunas formas en las que recomiendo que aprendan y se acerquen a ellas. Para los que sí están familiarizados con este tema, puede haber todavía algunas nuevas perspectivas interesantes, y particularmente en la última sección, formas útiles de acelerar su proceso de aprendizaje. En este artículo lo discutiremos: Tengan en cuenta que he intentado estructurar este artículo en una progresión lógica en la que cada sección (aparte de la siguiente, que trata más sobre cómo superar el miedo a sumergirse en este tema) se basa en la siguiente o la motiva. Estoy condensando más de mil horas de práctica y estudio en un artículo, y he hecho lo posible por explicar las cosas de forma clara y sencilla también.

Donde empezaron los problemas

A lo largo de mi limitada educación formal, no tuve la mejor relación con el campo de las matemáticas. Y por extensión, esto impactó mi relación con una gran cantidad de ciencias de la computación (CS) e ingeniería de software (SEng). En concreto, no es que sea malo en matemáticas, sino que soy pobre en aritmética y malo recordando fórmulas. También descubrí que la forma en que se enseñan las matemáticas, el CS y el SEng en las escuelas tampoco suele funcionar para mí. Mi propio proceso de aprendizaje hasta el día de hoy está impulsado en gran medida por el pragmatismo (énfasis en el conocimiento práctico sobre el teórico), la curiosidad sobre la naturaleza y cómo esta información puede ayudarme a ganarme la vida – tres cosas que rara vez vi enfatizadas en mi educación occidental. Aparte de mi incómoda relación con presentaciones secas y aburridas de cosas que son predominantemente de naturaleza matemática, soy un programador autodidacta. Para que quede claro, tomé una sola clase de programación en un colegio comunitario (alrededor de 2013), y el resto de mis conocimientos provienen de estudios autodirigidos. Durante los primeros años de ese proceso, también tuve que trabajar en varios trabajos de día para poder pagar el alquiler, lo que me dejó muy poco tiempo libre y energía para aprender mi oficio. El resultado final fue que elegí pasar la mayor parte de mi tiempo construyendo proyectos personales y aprendiendo temas específicamente para esos proyectos. Esto me llevó a ser muy bueno en la habilidad de escribir código, aprender nuevas tecnologías y resolver problemas. Sin embargo, si apliqué algún concepto en CS y SEng al código que estaba escribiendo, fue en gran parte por accidente. Para resumir esta introducción, trato de decir que el mayor obstáculo en mi estudio del SEng era que no estaba muy interesado en aprenderlo. No conocía el sentido de logro que se puede obtener al hacer un pequeño cambio en un algoritmo que reduce su tiempo de ejecución hasta su finalización en un factor de decenas o cientos de veces. No sabía lo importante que era elegir una estructura de datos basada en la naturaleza del problema que intentaba resolver, y mucho menos cómo tomar esa decisión. Y no tenía ni idea de lo relevante que era para mí ganarme la vida con aplicaciones para móviles. Así que en caso de que estén en el mismo barco, antes de que lleguemos a los detalles técnicos, me gustaría intentar responder a algunas de esas preguntas para usted. Para hacer las cosas menos aburridas, motivaré este tema compartiendo una historia real sobre lo que cambió mi actitud hacia este tema.

Nadando con el gran pez

A finales de 2019, me tomé un descanso en el estudio del desarrollo de Android para sumergirme en los sistemas operativos de UNIX y en la programación de C/C++. Me sentía muy cómodo con el SDK de Android, pero muchos años de programación de JVM me habían dejado con una fuerte sensación de no tener ni idea de cómo funcionaban realmente los ordenadores bajo el capó. Lo cual me molestaba bastante. No estaba buscando trabajo, pero en ese momento un reclutador de una gran empresa de tecnología me habló de mi interés en un puesto de ingeniero de software para Android. A pesar de estar muchos meses sin practicar con Android, lo hice bien en la primera entrevista (eran todos conceptos básicos de Android con los que estaba familiarizado) y me enviaron un correo electrónico detallando los temas a tratar en futuras entrevistas. La primera sección de este correo electrónico, que detalló el conocimiento específico de Android, fue extensa, pero al menos estaba algo familiarizado con la mayoría de los temas y no me intimidó. Sin embargo, cuando bajé a la sección de Estructuras de Datos y Algoritmos, de repente me sentí como cuando empecé a escribir código: Como un pez fuera del agua. No es que nunca haya aplicado ninguno de estos conceptos en mi código, pero ciertamente no he estudiado formalmente ninguno de ellos. Aunque haré todo lo posible por darte una introducción suave y clara a estos temas, SEng te golpeará inmediatamente con un muro de términos de la jerga, y mi cara estaba figurativamente muy dolorida y magullada después de leer toda la lista de DS y Algos que tuve que aprender en ese correo electrónico. Fui muy franco al respecto con mi reclutador, que amablemente me dio cuatro semanas para prepararme antes de la próxima entrevista. Sabía que no podría cubrir todos los temas en cuatro semanas, pero esperaba que aprender un año o dos de SEng en unas pocas semanas mostraría algo de talento e iniciativa. Me encantaría contarles una jugosa historia sobre cómo fracasé épicamente, o quedé completamente deslumbrado en la siguiente entrevista, pero la realidad es que las cosas se desmoronaron antes de que tuviera la oportunidad. Soy ciudadano canadiense, y el puesto requería el traslado a uno de los muchos campus de los Estados Unidos, ya sea en California o en el estado de Washington. A las dos semanas de mi primera inmersión profunda en el SEng, recibí un correo electrónico de mi reclutador diciendo que su departamento de inmigración no quería patrocinarme. Sospecho que tenía que ver con algunas dificultades para patrocinar a un trabajador sin título, pero la pandemia global que se está gestando también puede haber sido un factor. Al final, aunque quería tener la oportunidad de triunfar o fracasar en tiempo real, estaba feliz sabiendo que tenía una idea muy clara de los conocimientos que me faltaban para ser ingeniero de software en una gran empresa tecnológica. Con un camino claro pero difícil delante de mí, resolví no dejar que el campo de SEng me intimidara. Quería saber lo que realmente significa ser un desarrollador de software frente a un ingeniero de software. Con eso en mente, entraremos en las ideas centrales de SEng, y cómo hacer más fácil su aprendizaje. No es fácil, sólo más fácil.

Los “Tres Grandes” temas de la Ingeniería de Software – y por qué son importantes

Los principales temas de la ingeniería de software pueden resumirse utilizando un montón de grandes palabras y frases de miedo, como es la tradición en todo lo relacionado con la informática y las matemáticas. Para evitar confusiones, las explicaré usando el idioma inglés y ejemplos que priorizan la claridad por encima de todo. Le sugiero que siga esta sección en el orden que he establecido, ya que lo he estructurado deliberadamente en una progresión lógica.

Primero – ¿Qué son el tiempo de ejecución y el espacio de memoria?

Quiero empezar explicando la razón por la que estudiamos estos temas para empezar. Siendo un fanático de la física, me alegró saber que la interacción del tiempo y el espacio que vemos en la naturaleza también se observa directamente en cualquier tipo de computadora. Sin embargo, en este campo, nos referimos a estas cualidades como tiempo de ejecución y espacio de memoria. Para entender mejor lo que es el tiempo de ejecución, sugiero que saque su Administrador de Tareas, Monitor de Actividad, o cualquier programa que tenga que le diga sobre los “procesos” activos de su sistema Un proceso es sólo un “programa en ejecución”, y a través de la magia de tener múltiples “procesadores”, la virtualización de la CPU, y el corte de tiempo, puede parecer que tenemos decenas o cientos de procesos ejecutándose al mismo tiempo. He introducido esos términos de la jerga para que puedas buscarlos si tienes curiosidad por saber cómo funcionan los sistemas operativos, pero al hacerlo no es necesario seguir adelante con este artículo. En cualquier caso, el tiempo de ejecución de un proceso puede considerarse generalmente como cualquier punto en el tiempo durante el cual se puede visualizar en la herramienta de seguimiento de procesos de su sistema. Utilizo esta definición para señalar que un proceso activo no necesita tener una interfaz de usuario o incluso hacer algo útil, aunque todavía pueda estar ocupando tiempo de ejecución en el espacio de la CPU y la memoria. Hablando de espacio de memoria, para que algo tenga tiempo de ejecución, tiene que estar en algún lugar también. Ese algún lugar es el espacio de memoria física de la computadora, que está virtualizada (de nuevo, busque la virtualización en su propio tiempo pero no es necesaria para este artículo) para hacerla más segura y fácil de usar. A cada proceso se le asigna su propio espacio de memoria virtual distinto y protegido, que puede crecer o encogerse hasta ciertos límites y en función de diversos factores. Tomemos un descanso de la teoría para hablar de por qué debemos preocuparnos por ello. Dado que el tiempo de ejecución y el espacio de memoria pueden ser medidos con precisión pero también son limitados, los humanos como tú y yo podemos realmente arruinar las cosas si no prestamos atención a estas limitaciones! Para ser claros, aquí hay dos cosas muy importantes que queremos cuidar como programadores e ingenieros: Estas preguntas dictan en gran medida el éxito de nuestros programas, tanto si los estudias formalmente como si no. Con un poco de suerte te he motivado a aprender lo que yo llamo los tres grandes temas de la ingeniería de software, que vamos a tratar ahora.

Quizás también te interese  Los 5 mejores juegos de casino gratis para Android

Cómo medimos el tiempo de ejecución y el espacio de memoria

El primero de los tres grandes temas se describe usando un gran término de miedo: Tiempo de ejecución asintótica y complejidad espacial. Habiendo ya descrito el tiempo de ejecución y el espacio de memoria, creo que un sustituto útil para la palabra complejidad aquí es “eficiencia” Y la asintótica está relacionada con el hecho de que podemos representar esta eficiencia (o la falta de ella) en un gráfico cartesiano bidimensional. Ya sabes, x e y, subida sobre la carrera, y todas esas cosas. No te preocupes si no estás familiarizado con estas cosas. Sólo necesitas un entendimiento muy básico de estas cosas para aplicarlo en tu código. También, note que existe tal cosa como una estructura de datos de Gráfico, pero ese concepto está muy lejos de un Gráfico Cartesiano y no a lo que me refiero. Dado que podemos representar nuestro código y cómo se comporta con respecto al tiempo de ejecución o al espacio de memoria en un gráfico cartesiano, se deduce que debe haber funciones que describan cómo dibujar dicho gráfico. La forma en que describimos lo eficiente que es nuestro código de esta manera es usar la notación “Big O”. Aquí está la introducción más simple que puedo darles para entender este tema. Usaré el lenguaje de programación moderno Kotlin para mis ejemplos de código que espero les proporcione un feliz término medio para ustedes, desarrolladores web y nativos. Supongamos tres funciones (también conocidas a veces como métodos, algoritmos, comandos o procedimientos): Función printStatement: Función printArray: Función printArraySums: Para facilitar la comprensión, supongamos que cada vez que se llama a println(...), se tarda 100ms, o 1/10 de segundo de media en completarlo (en realidad 100ms para un solo comando de impresión es terriblemente lento pero es más fácil de imaginar que un microsegundo o un picosegundo). Teniendo esto en cuenta, pensemos críticamente en cómo se puede esperar que estas funciones se comporten de manera diferente, en función de las aportaciones que se les den. printStatement, salvo que se trate de un fallo catastrófico del propio sistema, siempre tardará un promedio de 100ms en completarse. De hecho, aunque la notación Big O está muy preocupada por el tamaño de los argumentos dados a una función (que tendrá más sentido en breve), esta función ni siquiera tiene argumentos para cambiar su comportamiento. Por lo tanto, podemos decir que la complejidad del tiempo de ejecución (el tiempo que tarda en completarse), es constante, lo que puede ser representado por la siguiente función matemática y gráfico: En el gráfico anterior, T representa el tiempo de ejecución de println(...) que establecimos como un promedio de 100 milisegundos. Explicaré a qué se refiere n momentáneamente. printArray presenta un nuevo problema. Es lógico que el tiempo que le tome a printArray completarse será directamente proporcional al tamaño del Array, arr, que se le pase. Si el Array tiene cuatro elementos, eso daría lugar a que se llamara a println(…) cuatro veces, para un tiempo de ejecución total promedio de 400ms para el propio printArray. Para ser más precisos matemáticamente, diríamos que la complejidad del tiempo de ejecución de printArray es lineal: printArraySums lleva las cosas un paso más allá en algo que debería preocuparte incluso como desarrollador de nivel junior o intermedio. El número de argumentos/entradas a cualquier función dada se refiere con una pequeña n, cuando se usa la notación Big O. En nuestra segunda función, esto se refiere exclusivamente al tamaño del Array (es decir, arr .size), pero en la tercera función se refiere al tamaño colectivo de los argumentos múltiples (es decir, arrOne y arrTwo). En la notación Big O, hay en realidad tres cualidades diferentes de una pieza de código dada a las que podemos prestar atención: En términos generales, en el mismo sentido en que un ingeniero civil está más preocupado por el número máximo de vehículos que puede soportar un puente, un ingeniero de software suele estar más preocupado por el rendimiento en el peor de los casos. Al observar printArraySums, debería ser capaz de razonar que podemos representar su complejidad de tiempo de ejecución en el peor de los casos (el número de veces que se llamará println(…)) como n * n; donde n está en o cerca del tamaño máximo permitido de un Array en el sistema. En caso de que no esté claro, no estamos simplemente emparejando y sumando los elementos de arrOne y arrTwo en los mismos índices, que están literalmente sumando cada valor de ellos juntos en un bucle anidado. A partir de aquí puedes empezar a entender realmente la importancia del tiempo de ejecución asintótica y la complejidad espacial. En el peor de los casos, el tiempo de ejecución crece exponencialmente en una curva cuadrática: Dos notas finales sobre este tema: En primer lugar, si de repente te he hecho temer un poco a los bucles anidados (y sí, cada bucle anidado añade potencialmente otro factor de n), entonces he hecho un buen trabajo. Aún así, entienda que si está seguro de que n no excederá un tamaño razonable incluso en una función que tiene un crecimiento exponencial, entonces no hay realmente un problema. Si desea saber cómo determinar si n tendrá un impacto negativo en el rendimiento, quédese para la última sección de este artículo. En segundo lugar, habrán notado que todos mis ejemplos se referían a la complejidad del tiempo de ejecución, no a la complejidad del espacio de memoria. La razón de esto es simple: Representamos la complejidad del espacio exactamente de la misma manera y con la misma notación. Dado que no asignamos ninguna nueva memoria aparte de una o dos referencias temporales dentro de cada marco de la para cada uno.. bucles, asintóticamente hablando, la segunda y la tercera función siguen siendo lineales, O(n), con respecto a la asignación de la memoria.

Estructuras de datos

El término Estructura de Datos, a pesar de lo que cualquier profesor pueda decirle, no tiene una definición singular. Algunos profesores harán hincapié en su naturaleza abstracta y en cómo podemos representarlos en las matemáticas, algunos profesores harán hincapié en cómo están dispuestos físicamente en el espacio de la memoria, y algunos harán hincapié en cómo se implementan en una especificación de lenguaje particular. Odio decirte esto, pero este es un problema muy común en la ciencia de la computación y la ingeniería: Una palabra que significa muchas cosas y muchas palabras que significan una cosa, todas al mismo tiempo. Por lo tanto, en lugar de tratar de hacer feliz a todo tipo de experto de todo tipo de antecedentes académicos o profesionales mediante el uso de una plétora de definiciones técnicas, permítanme molestar a todos por igual explicando las cosas lo más claramente posible en un inglés sencillo. Para los propósitos de este artículo, las Estructuras de Datos (DS) se refieren a las formas en que representamos y agrupamos los datos de nuestra aplicación en nuestros programas. Cosas como perfiles de usuario, listas de amigos, redes sociales, estados de juego, puntuaciones altas y así sucesivamente. Cuando se considera el DS desde la perspectiva física del hardware y el sistema operativo, hay dos formas principales de construir un DS. Ambas formas aprovechan el hecho de que la memoria física es discreta (una palabra de fantasía para referirse a la contabilidad), y por lo tanto direccionable. Una forma fácil de imaginarlo es pensar en las direcciones de las calles y en cómo, dependiendo de la dirección en la que se esté moviendo físicamente (y dependiendo de cómo organice su país las direcciones de las calles), la dirección aumenta o disminuye de valor.

Matriz física

La primera forma aprovecha el hecho de que podemos agrupar datos (por ejemplo, una lista de amigos en una aplicación de medios sociales) en un trozo de espacio de memoria contiguo (físicamente junto a cada uno). Esto resulta ser una forma muy rápida y eficiente para que una computadora atraviese el espacio de memoria. En lugar de darle a la computadora una lista de n direcciones para cada dato, le damos a la computadora una sola dirección que denota el inicio de este DS en la memoria física, y el tamaño de (es decir, n) el DS como un solo valor. La instrucción establecida para hacer esto podría ser tan simple como decirle a la máquina que se mueva de izquierda a derecha (o en cualquier dirección), disminuir el valor de n en 1 cada movimiento, y detenerse/retroceder cuando ese valor llegue a 0.

La segunda forma requiere que cada dato de la propia estructura contenga la(s) dirección(es) del elemento siguiente o anterior (¿tal vez ambos?) dentro de sí mismo. Uno de los grandes problemas de los espacios de memoria contiguos es que presentan problemas a la hora de crecer (añadiendo más elementos) o encogerse (esto puede fragmentar el espacio de memoria, lo cual no explicaré pero sugiero una rápida búsqueda en Google). Al hacer que cada dato se vincule a los otros (normalmente sólo al anterior o al siguiente), se vuelve en gran medida irrelevante dónde se encuentra cada dato en el espacio físico de la memoria. Por lo tanto, podemos aumentar o disminuir la estructura de los datos con relativa facilidad. Debería ser capaz de razonar que, dado que cada parte de la estructura almacena no sólo sus propios datos, sino la dirección del siguiente (o más que eso) elemento, entonces cada pieza requeriría necesariamente más espacio de memoria que con el enfoque de la matriz contigua. Sin embargo, si es más eficiente en última instancia, depende de qué tipo de problema esté tratando de resolver. Los dos enfoques que he discutido se conocen generalmente como una matriz y una lista de enlaces. Con muy pocas excepciones, la mayor parte de lo que nos interesa en el estudio del SD es cómo agrupar las colecciones de datos que tienen algún tipo de razón para ser agrupadas, y la mejor manera de hacerlo. Como traté de señalar, lo que hace que una estructura mejore en una determinada situación puede empeorarla en otra. De los párrafos anteriores se desprende que una Lista Vinculada suele ser más adecuada para una colección dinámica (cambiante), mientras que una Matriz suele ser más adecuada para una colección fija, al menos en lo que respecta al tiempo de ejecución y la eficiencia espacial. Sin embargo, no se deje engañar No siempre es el caso que nuestra principal preocupación es elegir el DS (o algoritmo) más eficiente con respecto al tiempo de ejecución y el espacio de memoria. Recuerde, si n es muy pequeño, entonces preocuparse por un nanosegundo o unos pocos bits de memoria aquí y allá no son necesariamente tan importantes como la facilidad de uso y la legibilidad. Lo último que me gustaría decir sobre el DS es que he observado una profunda falta de consenso sobre la diferencia entre un DS y un Tipo de Datos (DT). De nuevo, creo que esto se debe en gran medida a que diferentes expertos abordan esto desde diferentes ámbitos (matemáticas, circuitos digitales, programación de bajo nivel, programación de alto nivel) y al hecho de que es realmente bastante difícil hacer una definición verbal de uno que no describa al menos parcialmente (o totalmente) al otro. A riesgo de hacer la situación aún más confusa, a nivel puramente práctico , pienso en las estructuras de datos como cosas independientes del Sistema Tipo de un lenguaje de programación de alto nivel (suponiendo que tenga uno). Por otro lado, un tipo de datos es definido por y dentro de tal sistema de tipo. Pero sé que la Teoría de Tipos en sí misma es independiente de cualquier sistema de Tipos en particular, así que es de esperar que puedas ver lo difícil que es decir algo concreto sobre estos dos términos. Me llevó mucho tiempo explicar los dos temas anteriores porque me permiten introducir y motivar este tema con bastante facilidad. Antes de continuar, debo tratar brevemente de desenredar otro lío de jerga. Explicar el término “algoritmo” a mi manera, es en realidad muy simple: Un algoritmo es un conjunto de instrucciones (comandos) que pueden ser entendidos y ejecutados (actuados) por un Sistema de Procesamiento de Información (IPS). Por ejemplo, si se siguiera una receta para cocinar algo, entonces sería el IPS, el algoritmo sería la receta, y los ingredientes y los utensilios de cocina serían las entradas de datos (argumentos). Ahora, por esa definición las palabras función, método, procedimiento, operación, programa, guión, subrutina y algoritmo apuntan todas al mismo concepto subyacente. Esto no es por accidente, estas palabras significan fundamentalmente lo mismo. La confusión es que diferentes informáticos y diseñadores de idiomas implementarán (construirán) la misma idea de una manera ligeramente diferente. O incluso más deprimentemente, las construirán de la misma manera pero con un nombre diferente. Desearía que no fuera así, pero lo mejor que puedo hacer es advertirte. Eso es todo lo que necesitas saber sobre los algoritmos en general, así que seamos más específicos sobre cómo pueden ayudarnos a escribir mejor el código. Recordemos que nuestra principal preocupación como ingenieros de software es escribir un código que esté garantizado para ser eficiente (al menos de tal manera que mantenga a nuestros usuarios contentos) y seguro con respecto a los limitados recursos del sistema. También recuerde que he declarado anteriormente que algunos DS se desempeñan mejor que otros con respecto al tiempo de ejecución y el espacio de memoria, particularmente cuando n se hace grande. Lo mismo ocurre con los algoritmos. Dependiendo de lo que intente hacer, diferentes algoritmos se desempeñarán mejor que otros. También cabe destacar que el DS tenderá a dar forma a los algoritmos que se pueden aplicar al problema, por lo que seleccionar el DS y el algoritmo adecuados es el verdadero arte de la ingeniería de software. Para terminar este tercer tema principal, veremos dos formas comunes pero muy diferentes de resolver un problema: la búsqueda de una matriz ordenada. Por ordenado, quiero decir que está ordenado de menor a mayor, de mayor a menor, o incluso alfabéticamente. Además, supongamos que al algoritmo se le da algún tipo de valor objetivo como argumento, que es el que usamos para localizar un elemento en particular. Esto debería quedar claro en el ejemplo en caso de que haya alguna confusión. El problema del ejemplo es el siguiente: Tenemos una colección de Usuarios (tal vez cargada desde una base de datos o un servidor), que está ordenada de menor a mayor por un campo llamado userId, que es un valor entero. Supongamos que este userId viene de tomar el tiempo del sistema (busque Unix Time para más información) justo antes de crear el nuevo usuario. Redondeado al valor más pequeño que aún garantiza que no se repitan los valores. Si la frase anterior no tenía sentido, todo lo que necesitas saber es que esta es una colección ordenada sin repeticiones. Una forma sencilla de escribir este algoritmo sería escribir lo que llamaremos una Búsqueda Ingenua (NS). Ingenua, en este contexto, significa simple, pero de mala manera, lo que se refiere al hecho de que sólo le decimos al ordenador que empiece por un extremo de la colección y pase al otro hasta que encuentre una coincidencia con el índice de destino. Esto se logra generalmente utilizando algún tipo de bucle: Función naiveSearch: Si por casualidad sólo tenemos unos pocos cientos, o incluso unos pocos miles de usuarios en esta colección, entonces podemos esperar que esta función regrese bastante rápido de todos modos. Pero asumamos que estamos trabajando en una exitosa puesta en marcha de la tecnología de medios sociales, y acabamos de llegar a un millón de usuarios. Debería ser capaz de razonar que naiveSearch tiene la complejidad asintótica de O(n) como su peor caso de complejidad en tiempo de ejecución. La razón, en resumen, es que si el usuario objetivo se encuentra en n, entonces debemos invariablemente atravesar toda la colección para llegar allí. Si aún no está familiarizado con el algoritmo de Búsqueda Binaria (BS), entonces debería prepararse para que le vuelen la cabeza. ¿Y si te dijera que usando un algoritmo BS para buscar en nuestra colección un millón de elementos, sólo harás, como mucho, 20 comparaciones? Así es; 20 comparaciones (en lugar de 1 millón con NS) es el peor de los casos. Ahora, explicaré cómo funciona BS en principio, pero mi única tarea para ti es implementarlo en tu lenguaje de programación preferido. Puede ser que el lenguaje que elijas ya tenga una implementación de BS de su biblioteca estándar, pero este es un importante ejercicio de aprendizaje! En principio, en lugar de buscar una colección ordenada una por una, de forma unidireccional, empezamos mirando el valor en el índice n/2. Así que en una colección con 10 elementos, comprobaríamos el quinto elemento. El orden es importante, porque podemos comparar el elemento en n/2 con nuestro objetivo: Ahora, la idea es que estamos cortando el conjunto de datos a la mitad en cada iteración. Supongamos que el valor en el elemento n/2 es menor que nuestro valor objetivo. A continuación seleccionaríamos el índice medio entre n/2 y n. A partir de ahí, nuestro algoritmo sigue cortando hacia adelante o hacia atrás usando la misma lógica sobre un rango cada vez más pequeño de índices en nuestra colección. Esto nos lleva a la belleza del algoritmo BS aplicado a una colección clasificada: En lugar del tiempo que tarda en completarse creciendo linealmente, o exponencialmente con respecto a n, crece logarítmicamente: Si este artículo es realmente su primera introducción a las principales ideas de la ingeniería de software, entonces por favor no espere que todo tenga sentido inmediatamente. Aunque espero que algunas de mis explicaciones hayan ayudado, mi objetivo principal era darles una lista básica de ideas para estudiarlas, y lo que creo que es un buen orden para estudiar esas ideas. El siguiente paso para ti es hacer un plan para aprender este campo, y tomar medidas al respecto. La siguiente sección trata sobre cómo hacerlo.

Quizás también te interese  Envíe notificaciones por SMS o por WhatsApp desde el software ERP

Cómo aprender ingeniería de software – Algunos consejos prácticos

Ahora discutiré algunas ideas y enfoques que he usado personalmente para hacer el proceso de aprendizaje de varios DS y algoritmos más fácil (¡pero no fácil!), tanto en la práctica como desde una perspectiva motivacional. Confío en que habrá al menos uno o dos puntos aquí que serán útiles (suponiendo que no los haya alcanzado usted mismo), pero también quiero hacer hincapié en que nuestros cerebros podrían funcionar de forma ligeramente diferente. Coge lo que es útil y tira el resto. Como ayuda para el aprendizaje, tal vez quieras ver mi lección en vivo en youtube donde cubro este tema. No te saltes el resto de este artículo; ¡Entré en mucho más detalle aquí!

Siga un enfoque de aprendizaje basado en proyectos

Esto es lo primero que le digo a cualquier programador nuevo que me pregunte la “mejor manera” de aprender a codificar. He dado una versión más larga de esta explicación muchas veces, pero resumiré la idea general. En cualquier campo de la programación, notarán que hay un número increíblemente grande de temas para estudiar, y el campo en sí mismo está en constante evolución tanto para los académicos como para los profesionales de la industria. Como mencioné en la introducción, no sólo no tenía un plan de estudios que guiara mis estudios, sino que también tenía un tiempo limitado para estudiar porque tenía que pagar el alquiler al final de cada mes. Esto me llevó a la necesidad de desarrollar y seguir mi enfoque de aprendizaje basado en proyectos. En esencia, lo que sugiero que hagas es evitar aprender DS y algoritmos simplemente estudiando las cosas tema por tema y tomando notas sobre cada uno. En su lugar, comenzarás por elegir un tema básico (como los que he cubierto antes) e inmediatamente escribirás un fragmento de código o una pequeña aplicación que lo utilice. Creé un repositorio que tenía un paquete para cada familia de DS y algoritmo que quería aprender. Para los algoritmos generales era sobre todo la clasificación y las búsquedas (Bubble Sort, Merge Sort, Quick Sort, Binary Search, etc.). Para DS más específicos como Linked Lists, Trees, Heap, Stack, y otros, escribí tanto el DS en sí como algunos algoritmos específicos para ese DS en particular. Ahora, encontré que algunos tipos de DS eran difíciles de entender e implementar al principio. Una familia de DS que me dio problemas durante bastante tiempo se llamaban “Gráficos” El campo en general está lleno de alguna jerga particularmente horrible y sobrecargada, pero este tema en particular incluso tiene un nombre engañoso (pista: un nombre mejor sería “Redes”). Después de dar vueltas a mis ruedas durante varias semanas (aunque para ser justos estaba aprendiendo esto de lado), finalmente admití para mí mismo que necesitaba una razón clara para usar esta DS en algún código de aplicación. Algo que justificara y motivara las muchas horas que iba a pasar aprendiendo este tema. Habiendo construido previamente un juego de Sudoku usando algoritmos que funcionaban con matrices bidimensionales y unidimensionales, recordé haber leído en algún lugar que era posible representar y resolver un juego de Sudoku usando un gráfico de color no dirigido. Esto fue increíblemente útil, ya que estaba familiarizado con el dominio del problema del Sudoku, así que pude concentrarme intensamente en el DS y los algoritmos. Aunque hay mucho más que tengo que aprender, no puedo describir lo satisfactorio que fue cuando escribí un algoritmo que generó y resolvió 102 rompecabezas de Sudoku en 450 milisegundos. Hablando de eso, déjame hablar de otra forma de escribir mejores algoritmos que también pueden ser una gran fuente de motivación y establecimiento de objetivos.

Quizás también te interese  Las actualizaciones navideñas de tus juegos favoritos en Android: Among US, PUBG, Genshin Impact…

Pruebe su código

Mira, sé que mucha gente hace del tema de las pruebas una completa pesadilla para los principiantes. Esto sucede porque confunden la muy simple idea de cómo probar el código con algunas herramientas muy elaboradas y confusas que uno puede usar opcionalmente para probar su código. Pero esta es importante, así que por favor, quédate conmigo. Para volver a lo básico, sin siquiera hablar de la notación de Big O, ¿cómo sabemos si un algoritmo es más eficiente que otro? Por supuesto, tenemos que probar ambos. Ahora bien, es importante mencionar que las pruebas de referencia pueden dar una buena (o incluso gran) idea general, pero también están fuertemente influenciadas por el sistema en el que se prueban. Cuanto más precisas sean sus pruebas, más preocupados estarán por el entorno, la configuración y la precisión de las mismas. Sin embargo, para el tipo de código que suelo escribir, una buena idea general es todo lo que necesito. Hay dos tipos de pruebas que me resultan muy útiles para cuando escribo mis algoritmos, tanto para la práctica como para el código de producción. El primer tipo de prueba responde a una pregunta muy simple: ¿Funciona? Para tomar un ejemplo de mi aplicación de Sudokus Gráfico, uno de los primeros obstáculos para mí fue construir lo que se llama una Lista de Adyacencia para Sudokus de diversos tamaños (probé 4, 9, 16 y 25, que son, no por accidente, cuadrados perfectos (matemáticamente hablando). No puedo explicar en detalle lo que es una Lista de Adyacencia, pero piense en ella conceptualmente como una Red de nodos y líneas (llamadas bordes por alguna razón). En la práctica, forma la estructura virtual del “Gráfico” En las reglas del Sudoku, cada columna, fila o subgrupo de números no puede contener ninguna repetición. De estas reglas, podemos deducir que en un Sudoku de 9×9, debe haber 81 nodos (uno por cada número), y cada nodo debe poseer 21 aristas (una por cada otro nodo en una determinada columna, fila o subcuadrícula). El primer paso fue simplemente comprobar que estaba construyendo el número correcto de nodos: El algoritmo para eso era realmente bastante fácil de escribir, pero las cosas eran ligeramente más difíciles para el siguiente. Ahora necesitaba, como dicen en la jerga de los gráficos, construir los bordes. Esto era un poco más complicado ya que tenía que escribir algunos algoritmos para seleccionar las filas, columnas y subgrupos de tamaño dinámico. De nuevo, para confirmar que iba por el buen camino, escribí otra prueba: A veces seguí el enfoque de Desarrollo Dirigido por Pruebas (TDD) y escribí las pruebas antes de los algoritmos, y a veces escribí las pruebas después de los algoritmos. En cualquier caso, una vez que pude verificar la corrección de cada algoritmo hasta el punto de que estaba generando un rompecabezas de Sudoku resuelto y de tamaño variable, era el momento de escribir un conjunto diferente de pruebas: ¡Puntos de referencia! Este tipo particular de pruebas de referencia es bastante contundente, pero eso es todo lo que necesitaba. Para probar la eficiencia de mis algoritmos, que en esta etapa podrían construir y resolver un Sudoku generado al azar, escribí una prueba que generó 101 rompecabezas de Sudoku: Inicialmente tuve dos llamadas a System.nanoTime() inmediatamente antes y después de generar 100 rompecabezas, y resté la diferencia para obtener un número ininteligible. Sin embargo, mi IDE también llevaba un registro de cuánto tiempo tardaba en completarse una prueba en minutos, segundos y milisegundos, así que finalmente me decidí por eso. La primera serie de pruebas de referencia (para los puzles de 9×9) fue como sigue: Aunque no tenía mucho punto de referencia, sabía que estaba tardando más de un segundo en generar un Sudoku de 9×9, lo cual era una muy mala señal. No estaba contento de cómo estaba precargando el gráfico con algunos números válidos de antemano, así que decidí refactorizar mi enfoque allí. Naturalmente, el resultado después de pensar en una nueva forma de hacerlo fue peor: Después de unos cuantos puntos de referencia más que parecían empeorar ligeramente con el tiempo, estaba bastante desmoralizado y me preguntaba qué hacer. Tenía lo que pensaba que era una forma muy ingeniosa de hacer mi algoritmo más o menos quisquilloso basado en lo seguro que era colocar un número en el rompecabezas. Sin embargo, no funcionaba tan bien en la práctica. Como suele ocurrir, en aproximadamente la 400ª pasada de mi código a través de un codificador (parte de una herramienta de depuración), me di cuenta de que tenía un pequeño error que tenía que ver con la forma en que estaba ajustando el valor que dictaba lo exigente que era mi algoritmo. Lo que pasó después me dejó alucinado. Hice otra prueba de referencia y obtuve un resultado extraño: Estaba completamente incrédulo, así que lo primero que hice fue deshacer el cambio que acababa de hacer y repetir la prueba. Después de 5 minutos detuve la prueba ya que era claramente la diferencia de cambio del juego, y procedí a ejecutar cinco puntos de referencia más: Sólo por diversión, decidí intentar construir 101 rompecabezas de 16×16. Anteriormente ni siquiera podía construir uno de estos (al menos dejé de intentarlo después de que la prueba funcionara durante 10 minutos): El punto que estoy tratando de comunicar es este: No es sólo que las pruebas de escritura me permitieron verificar que mis algoritmos funcionaban. Me permitieron tener una forma objetiva de establecer su eficiencia. Por extensión, esto me dio una forma muy clara de saber cuál de los 50 ajustes diferentes del algoritmo que hice tuvo realmente un efecto positivo o negativo en el resultado. Esto es importante para el éxito de la aplicación, pero también fue increíblemente positivo para mi propia motivación y salud psicológica. Lo que aún no he mencionado es que el tiempo que me llevó pasar del primer punto de referencia al quinto (el rápido), fue de aproximadamente 40 horas (cuatro días a 10 horas por día). Al cuarto día estaba bastante desmoralizado, pero cuando por fin ajusté las cosas de la manera correcta, fue la primera vez que me sentí como un verdadero ingeniero de software en lugar de alguien que lo estudia por diversión. Para dejarles con una gran imagen, después de hacer las pruebas de 16×16 y ver que eran prometedoras, me tomé 15 minutos completos para correr alrededor de mi propiedad rural gritando como un chimpancé excitado que acaba de ser dosificado con adrenalina.

Mi sugerencia final

Haré que esta sea corta y dulce. Lo peor que puedes hacer como estudiante, al no poder entender algo difícil y complicado, es culparte a ti mismo. Los buenos maestros y las explicaciones son raros, y esto es particularmente cierto en los temas que la mayoría de nosotros encontramos relativamente secos y aburridos. Tuve que ver aproximadamente cuatro videos, leer aproximadamente cinco artículos/capítulos de libros de texto, y sumergirme ciegamente en la escritura de algún código que inicialmente no tenía sentido para mí, sólo para empezar con los gráficos. Esto puede ser una buena o mala noticia para usted, pero yo trabajo muy duro en lo que hago, y muy rara vez encuentro cosas en mi campo que sean naturales o fáciles para mí. Mi objetivo con este artículo nunca ha sido dar a entender que el aprendizaje de la ingeniería de software fue fácil para mí, ni que será fácil para usted. La diferencia es que de vez en cuando me dicen que explico bien las cosas, y que a diferencia de la gente que sólo regurgita lo que dicen los otros profesores, yo dedico el tiempo a averiguar lo que me funciona o no me funciona, e intento compartirlo con vosotros. Espero de verdad que algo de este artículo le haya sido útil. ¡Buena suerte con tus objetivos de aprendizaje, y feliz codificación!

Antes de que te vayas…

Si te gusta lo que escribo, probablemente te guste mi contenido de video. Creo de todo, desde tutoriales dedicados a temas específicos, a sesiones semanales de preguntas y respuestas en vivo, a maratones de codificación de más de 10 horas donde construyo un toda la aplicación en una sola sesión.

Ir al contenido