Una buena arquitectura consiste en optimizar tres requisitos:
- Los usuarios deben estar satisfechos con el rendimiento, la usabilidad y la solidez.
- Los desarrolladores actuales deben poder leer / escribir código de manera eficiente y sin errores.
- Los futuros desarrolladores deben poder extender y mejorar el código fácilmente.
Desafortunadamente, es casi imposible satisfacer los tres requisitos simultáneamente. A veces necesitamos aumentar la complejidad para obtener las funciones del usuario. O a veces necesitamos sacrificar la extensibilidad para que sea más fácil para los desarrolladores. Y los tres tienden a sacrificarse cuando hay restricciones de recursos impuestas por el negocio.
No existe un algoritmo simple para llegar a una buena arquitectura porque las compensaciones entre los tres requisitos dependen de los detalles del proyecto. La experiencia y la práctica son las únicas formas de desarrollar la intuición para hacer las compensaciones correctas. Así que no importa lo que haga, practique en sus propios proyectos e intente evaluar la arquitectura resultante con respecto a los tres requisitos anteriores.
- ¿Cuáles son algunos ejemplos épicos de bikeshedding?
- ¿Cómo puede alguien ser un excelente desarrollador al lidiar con las presiones de la universidad?
- ¿Hay alguna diferencia entre el "bloqueo" de dos fases y el "compromiso" de dos fases?
- Cómo conseguir un trabajo de software en Google India
- ¿Qué sucede si dos UUID son conflictivos?
Déjame pasar por un ejemplo:
Probablemente estés familiarizado con el juego Asteroides . Es un juego simple en el que pilotas una nave espacial (en el medio) y disparas a los asteroides. Si un asteroide te golpea, mueres. Pero si le disparas a un asteroide, se divide en asteroides más pequeños.
Intentemos implementar este juego en un lenguaje típico orientado a objetos. ¿Qué clases definirías? ¿Cuáles son sus métodos? ¿Cómo interactuarían? Esta es una gran parte de la definición de la arquitectura.
Podríamos ingenuamente comenzar con dos clases: una para el barco ( CShip ) y otra para los asteroides ( CAsteroid ). Cada clase es responsable de pintarse y actualizar su movimiento. Y, por supuesto, cada uno podría descender polimórficamente de la clase abstracta CSpaceObject .
Hasta ahora todo bien, pero ¿y las balas? ¿Cómo representamos las balas? ¿Debería cada viñeta ser su propio objeto (una clase CBullet )? ¿Debería el jugador enviar un seguimiento de sus propias balas? ¿Debería haber una sola clase para rastrear todas las balas? ¿Cuáles son los pros y los contras?
- Si las viñetas son su propio objeto, entonces la ventaja es que podemos implementar más fácilmente diferentes tipos de viñetas. Pero la desventaja podría ser el rendimiento: los objetos pueden ser caros.
- Si incluimos balas como parte de la nave, entonces podríamos simplificar la programación (encapsular balas en un lugar), pero podríamos tener dificultades si decidimos más tarde que otros tipos de objetos también disparan balas.
- Si tenemos una sola clase para rastrear todas las viñetas, podríamos mejorar el rendimiento, pero podríamos aumentar la complejidad del desarrollador. Por ejemplo, las pruebas de colisión se vuelven más complicadas si algunas cosas son objetos separados y otras no.
Para este proyecto en particular, implementaría cada viñeta como un objeto separado (CBullet descendiendo de CSpaceObject). Pero, ¿qué pasaría si este fuera un juego multijugador masivo con cientos de jugadores disparando balas? ¿Funcionaría la misma arquitectura? Tal vez no.
Considere la prueba de colisión. Imagine que comenzamos con una lista de todos los CSpaceObjects , que incluye barco, asteroides y balas. Podemos recorrer cada objeto y ver si está colisionando con cualquier otro objeto. Si es así, llamamos a un método para resolver la colisión. Si un asteroide golpea a un asteroide, no hacemos nada. Pero si una bala golpea un asteroide, lo rompemos.
Esta es una arquitectura simple y extensible. ¿Pero funcionará? Para n objetos necesitamos hacer [( n – 1) * n ] / 2 comparaciones. Para 100 objetos, eso es casi 5,000 comparaciones. ¡Para 1,000 objetos, es medio millón de comparaciones! Claramente esto no escalará demasiado lejos.
Podemos mejorar esto observando que estamos haciendo pruebas de colisión innecesarias. No sucede nada cuando un asteroide golpea a un asteroide, entonces, ¿por qué hacer pruebas de colisión? Pero eso significa que ya no podemos tratar a todos nuestros objetos por igual. En lugar de tener una gran lista de todos los objetos, quizás necesitemos diferentes listas para cada tipo de objeto.
Incluso en este sencillo ejemplo, puede ver la tensión entre extensibilidad, complejidad y rendimiento. Si queremos crear nuevos tipos de objetos (barco enemigo, agujeros de gusano, lo que sea), queremos que el código principal trate los objetos genéricamente y mantenga las diferencias en la implementación.
Pero si necesitamos soportar miles de objetos, necesitamos separar diferentes tipos de objetos y tratarlos de manera diferente, o crear estructuras de datos adicionales (por ejemplo, árboles de partición de espacio binario) para manejar las colisiones de manera eficiente. De cualquier manera estamos agregando más complejidad.
Ser bueno en arquitectura significa prever esta tensión desde el principio. Nunca podrá predecir el futuro, pero tener en cuenta este tipo de preguntas desde el principio podría ayudarlo a crear una arquitectura duradera.