Hermanos, maestros, ¿cómo mejorar su pensamiento en programación?
Capítulo 1 Introducción a los Objetos
“¿Por qué la programación orientada a objetos tiene un impacto tan impactante en el campo del desarrollo de software?”
Programación orientada a objetos (OOP) ) tiene muchos aspectos atractivos. Para los administradores, permite procesos de desarrollo y mantenimiento más rápidos y económicos. Para los analistas y diseñadores, el proceso de modelado se vuelve más simple y produce diseños claros y fáciles de mantener. Para los programadores, el modelo de objetos parece muy elegante y simple. Además, el poder de las bibliotecas y herramientas orientadas a objetos hace que la programación sea una tarea más agradable. Todos pueden beneficiarse, al menos superficialmente.
Si tiene un inconveniente es el precio que pagas por dominarlo. Al pensar en objetos, es necesario utilizar el pensamiento de imágenes en lugar del pensamiento de procedimientos. El proceso de diseño de objetos es más desafiante que el diseño procedimental, especialmente cuando se intenta crear objetos reutilizables (renovables). En el pasado, los nuevos en el campo de la programación orientada a objetos tenían que tomar una decisión dolorosa:
(1) Elegir un lenguaje como Smalltalk, que requiere dominar una biblioteca enorme antes de "empezar".
(2) Elija C (nota ①), que casi no tiene bibliotecas, y luego aprenda este lenguaje en profundidad hasta que pueda escribir su propia biblioteca de objetos.
①: Afortunadamente, esta situación ha cambiado significativamente. Ahora hay disponibles bibliotecas de terceros, así como bibliotecas C estándar.
De hecho, es difícil diseñar bien objetos y, por tanto, diseñar bien cualquier cosa. Como resultado, sólo un número relativamente pequeño de "expertos" puede diseñar los mejores objetos para que otros disfruten. Para que los lenguajes POO tengan éxito, no solo integran la sintaxis del lenguaje junto con un compilador (compilador), sino también un entorno de desarrollo exitoso con bibliotecas bien diseñadas y fáciles de usar. Por lo tanto, la primera tarea de la mayoría de los programadores es utilizar objetos existentes para resolver sus propios problemas de aplicación. El objetivo de este capítulo es exponer los conceptos de programación orientada a objetos y demostrar lo simple que es.
Este capítulo le explicará muchas ideas de diseño de Java y le explicará conceptualmente la programación orientada a objetos. Pero tenga en cuenta que después de leer este capítulo, no podrá escribir un programa Java con todas las funciones de inmediato. Todas las instrucciones y ejemplos detallados se explicarán en otros capítulos de este libro.
1.1 Progreso en la abstracción
El objetivo final de todos los lenguajes de programación es proporcionar un método "abstracto". Una afirmación más controvertida es que la complejidad de la resolución de problemas depende directamente del tipo y calidad de la abstracción. ¿El "tipo" aquí se refiere a lo que se está "abstrayendo"? El lenguaje ensamblador es una pequeña abstracción de la máquina subyacente. Muchos lenguajes "imperativos" posteriores (como FORTRAN, BASIC y C) fueron una abstracción del lenguaje ensamblador. Estos lenguajes han recorrido un largo camino en comparación con el lenguaje ensamblador, pero sus principios abstractos aún requieren que nos centremos en la estructura de la computadora en lugar de en la estructura del problema en sí. El programador debe establecer una conexión entre el modelo de máquina (ubicado en el "espacio de solución") y el modelo del problema que realmente se está resolviendo (ubicado en el "espacio de problema"). Este proceso requiere que las personas pongan mucho esfuerzo y, debido a que rompe con el alcance del propio lenguaje de programación, hace que el código del programa sea difícil de escribir y costoso de mantener. Un efecto secundario de esto es una disciplina bien establecida de "métodos de programación".
Otra forma de modelar una máquina es realizar un modelo del problema a resolver. Para algunos de los primeros lenguajes, como LISP y APL, su enfoque era "observar el mundo desde una perspectiva diferente": "todos los problemas se reducen a listas" o "todos los problemas se reducen a algoritmos". PROLOG resume todos los problemas en cadenas de toma de decisiones. Para estos lenguajes, creemos que están en parte orientados a la programación basada en "fuerza" y en parte diseñados para tratar con símbolos gráficos. Cada método tiene su propio propósito especial y es adecuado para resolver un determinado tipo de problema. Pero mientras estén más allá de sus capacidades, parecerán muy torpes.
La programación orientada a objetos ha dado un gran paso adelante sobre esta base. Los programadores pueden utilizar algunas herramientas para expresar elementos en el espacio del problema. Debido a que esta expresión es tan general, no tiene por qué limitarse a un tipo específico de pregunta. A los elementos del espacio del problema y a sus representaciones en el espacio de la solución los llamamos "Objetos". Por supuesto, hay otros objetos que no tienen equivalentes en el espacio del problema. Al agregar nuevos tipos de objetos, el programa se puede adaptar de manera flexible para adaptarse a problemas específicos. Entonces, cuando leas el código de descripción del plan, leerás las palabras que expresan el problema. Sin duda, este es un enfoque más flexible y poderoso para la abstracción del lenguaje que el que hemos visto antes. En resumen, la programación orientada a objetos nos permite describir problemas en términos de problemas, no en términos de soluciones. Sin embargo, todavía hay un vínculo con la computadora. Cada objeto es como una pequeña computadora; tienen su propio estado y se les pueden solicitar operaciones específicas. En comparación con los "objetos" u "objetos" del mundo real, los "objetos" de programación también tienen las similitudes más comunes: todos tienen sus propias características y comportamientos.
Alan Kay resumió las cinco características básicas de Smalltalk. Este fue el primer lenguaje de programación orientado a objetos exitoso y la base de Java. A través de estas características, podemos entender cómo es un enfoque de programación orientado a objetos "puro":
(1) Todo es un objeto. Piense en un objeto como un nuevo tipo de variable; contiene datos, pero puede pedirle que realice operaciones sobre sí mismo. En teoría, todos los componentes conceptuales pueden derivarse del problema a resolver y luego expresarse como un objeto en el programa.
(2) Un programa es una combinación de una gran cantidad de objetos; a través del paso de mensajes, cada objeto sabe lo que debe hacer. Para realizar una solicitud a un objeto, es necesario "enviar" un mensaje a ese objeto. Más específicamente, piense en un mensaje como una solicitud para llamar a una subrutina o función que pertenece al objeto de destino.
(3) Cada objeto tiene su propio espacio de almacenamiento que puede acomodar otros objetos. En otras palabras, al encapsular objetos existentes, se pueden crear nuevos tipos de objetos. Entonces, aunque el concepto de objeto es muy simple, puede alcanzar un nivel arbitrariamente alto de complejidad en un programa.
(4) Todo objeto tiene un tipo. Según la sintaxis, cada objeto es una "instancia" de una determinada "clase". Entre ellos, "Clase" es sinónimo de "Tipo". La característica más importante de una clase es "¿Qué mensajes se le pueden enviar?".
(5) Todos los objetos de la misma clase pueden recibir el mismo mensaje. En realidad, esta es una afirmación con un significado diferente y pronto todos la entenderán. Dado que un objeto de tipo "Círculo" también es un objeto de tipo "Forma", un círculo es totalmente capaz de recibir mensajes de formas. Esto significa que el código del programa se puede unificar para controlar la "forma", de modo que pueda controlar automáticamente todos los objetos que coincidan con la descripción de la "forma", que naturalmente incluye el "círculo". Esta característica se denomina "reemplazabilidad" de objetos y es uno de los conceptos más importantes de la programación orientada a objetos.
Algunos diseñadores de lenguajes creen que la programación orientada a objetos en sí misma no es suficiente para resolver fácilmente todas las formas de problemas de programación y abogan por combinar diferentes métodos en un "lenguaje de programación polimórfico" (nota ②).
②: Ver "Programación multiparadigma en Leda" editado por Timothy Budd, publicado por Addison-Wesley en 1995.
1.2 Interfaz de objeto
Aristóteles fue quizás la primera persona en estudiar seriamente el concepto de "tipo". Una vez habló del problema de los "peces y los pájaros". En Simula-67, el primer lenguaje orientado a objetos del mundo, se utilizó por primera vez este concepto:
Todos los objetos, aunque tienen sus propias características, pertenecen a una parte de una serie de objetos. Estos objetos tienen características y comportamientos comunes.
En Simula-67, se usó la palabra clave class por primera vez, lo que introdujo un nuevo tipo en el programa (clas y type generalmente se usan indistintamente; nota ③).
③: Algunas personas hacen una distinción adicional y enfatizan que el "tipo" determina la interfaz y la "clase" es una implementación especial de esa interfaz.
Simula es un buen ejemplo. Como sugiere el nombre, su función es "simular" problemas clásicos como el "cajero de banco". En este ejemplo, tenemos una lista de cajeros, clientes, cuentas, transacciones, etc. Cada tipo de miembro (elemento) tiene algunas características comunes: cada cuenta tiene un saldo determinado; cada cajero puede recibir depósitos de los clientes, etc. Al mismo tiempo, cada miembro tiene su propio estatus; cada cuenta tiene un saldo diferente y cada cajero tiene un nombre; Por lo tanto, en un programa de computadora, se pueden utilizar entidades únicas para representar cajeros, clientes, cuentas y transacciones. Esta entidad es el "objeto", y cada objeto pertenece a una "clase" específica, que tiene sus propias características y comportamiento generales.
Por tanto, en la programación orientada a objetos, aunque lo que realmente tenemos que hacer es crear varios "tipos" de datos nuevos (Type), casi todos los lenguajes de programación orientados a objetos adoptan la "clase" palabra clave. Cuando vea la palabra "tipo", piense también en "clase" y viceversa;
Después de construir una clase, se pueden generar muchos objetos según la situación. Luego, esos objetos pueden procesarse como elementos presentes en el problema a resolver. De hecho, cuando hacemos programación orientada a objetos, uno de los mayores desafíos que enfrentamos es: cómo conciliar los elementos del "espacio del problema" (donde el problema realmente existe) con el "espacio de la solución" (el lugar donde el problema real existe). se modela el problema). Establecer una correspondencia ideal "uno a uno" o una relación de mapeo entre elementos de un lugar (como una computadora).
¿Cómo utilizar objetos para realizar un trabajo verdaderamente útil? Debe haber una manera de solicitar al objeto que haga algo práctico, como completar una transacción, dibujar algo en la pantalla o encender un interruptor. Cada objeto sólo puede aceptar solicitudes específicas. La solicitud que hacemos a un objeto se define a través de su "Interfaz", y el "tipo" o "clase" del objeto especifica su forma de interfaz. La equivalencia o correspondencia entre "tipo" e "interfaz" es la base de la programación orientada a objetos.
Tomemos una bombilla como ejemplo:
Light lt = new Light();
lt.on();
En este ejemplo, el nombre de tipo/clase es Luz, y las solicitudes que se pueden realizar al objeto Luz incluyen encenderlo (encenderlo), apagarlo (apagarlo), hacerlo más brillante (iluminar) o atenuarlo. (oscuro). Simplemente declarando un nombre (lt), creamos un "identificador" para el objeto Light. Luego use la nueva palabra clave para crear un objeto de tipo Luz. Luego use el signo igual para asignarlo al identificador. Para enviar un mensaje a un objeto, enumeramos el nombre del identificador (lt) y lo conectamos al nombre del mensaje (on) con un punto (.). Se puede ver que al usar algunas clases predefinidas, el código que usamos en el programa es muy simple e intuitivo.
1.3 Ocultar el plan de implementación
Para facilitar la discusión posterior, clasifiquemos primero a los profesionales en este campo. Fundamentalmente, hay aproximadamente dos tipos de personas involucradas en la programación orientada a objetos: "creadores de clases" (personas que crean nuevos tipos de datos) y "programadores clientes" (personas que adoptan tipos de datos existentes en sus propias aplicaciones). Para los programadores de clientes, el objetivo principal es recopilar una "caja de herramientas" de programación llena de varias categorías para desarrollar rápidamente aplicaciones que cumplan con sus propios requisitos. Para los creadores de clases, su objetivo es crear una clase desde cero, abriendo solo las cosas necesarias (interfaces) a los programadores cliente y ocultando todos los demás detalles.
¿Por qué hacer esto? Después de ocultarse, los programadores cliente no pueden acceder ni cambiar esos detalles, por lo que el autor original no tiene que preocuparse de que sus obras sean modificadas ilegalmente y puede asegurarse de que no afectarán a otros.
④: Gracias a mi amigo Scott Meyers, que me ayudó a pensar en este nombre.
Una "Interfaz" especifica qué solicitudes se pueden realizar para un objeto específico. Sin embargo, debe existir algún código en alguna parte para satisfacer estas solicitudes. Este código y los datos ocultos se denominan "implementación oculta". Desde la perspectiva de la programación de procedimientos (Programación de procedimientos), todo el problema no parece complicado. Un tipo contiene funciones asociadas con cada posible solicitud. Una vez que se realiza una solicitud específica al objeto, se llama a esa función. Generalmente resumimos este proceso como "enviar un mensaje" (realizar una solicitud) al objeto. Es responsabilidad del objeto decidir cómo reaccionar ante este mensaje (ejecutar el código correspondiente).
Como ocurre con cualquier relación, es importante que todos los involucrados sigan las mismas reglas. Cuando creas una biblioteca, estableces una relación con el programador cliente. La otra persona también es programador, pero su objetivo es crear una aplicación (programa) específica o utilizar su biblioteca para construir una biblioteca más grande.
Si todos pueden usar todos los miembros de una clase, entonces los programadores cliente pueden hacer cualquier cosa con esa clase y no hay forma de obligarlos a obedecer ninguna restricción. Incluso si un programador cliente es muy reacio a manipular directamente algunos miembros contenidos dentro de una clase, sin control de acceso, no hay manera de evitar que esto suceda: todo quedará expuesto.
Hay dos razones por las que controlamos el acceso a los miembros. La primera razón es evitar que los programadores toquen cosas que no deberían (generalmente ideas de diseño de tipos de datos internos). Si es solo para resolver un problema específico, el usuario solo necesita operar la interfaz y no necesita comprender la información. Lo que ofrecemos a los usuarios es en realidad un servicio porque pueden ver fácilmente qué es importante para ellos y qué pueden ignorar.
La segunda razón para el control de acceso es permitir a los diseñadores de bibliotecas modificar las estructuras internas sin preocuparse por el impacto que tendrá en los programadores del cliente. Por ejemplo, inicialmente podríamos diseñar una clase con un formulario simple para simplificar el desarrollo. Posteriormente se decidió reescribirlo para hacerlo más rápido. Si la interfaz y los métodos de implementación se han aislado y protegido por separado, puede hacerlo con confianza y solo requerirá que el usuario se vuelva a conectar.
Java utiliza tres palabras clave explícitas (explícitas) y una palabra clave implícita (implícita) para establecer límites de clase: público, privado, protegido e implícito amigable. Si no se especifica explícitamente ninguna otra palabra clave, el valor predeterminado es esta última. El uso y significado de estas palabras clave son bastante intuitivos y determinan quién puede utilizar el contenido de definición posterior. "Público" significa que las definiciones posteriores están disponibles para cualquiera. "Privado", por otro lado, significa que nadie más que usted, el creador del tipo y los miembros de la función interna de ese tipo, pueden acceder a la información de definición posterior. privado levanta un muro entre usted y los programadores de sus clientes. Si alguien intenta acceder a un miembro privado, obtendrá un error en tiempo de compilación. "Amigable" se refiere al concepto de "paquete" o "paquete", la forma en que Java utiliza para crear bibliotecas. Si algo es "amigable", significa que sólo se puede utilizar dentro del alcance de este paquete (por lo que este nivel de acceso a veces se denomina "acceso al paquete"). "protegido" es similar a "privado", excepto que una clase heredada puede acceder a miembros protegidos pero no a miembros privados. La cuestión de la herencia se discutirá en breve.
1.4 Reutilización de soluciones
Una vez que una clase ha sido creada y probada, debería (idealmente) representar una unidad de código útil.
Pero, a diferencia de lo que mucha gente espera, esta capacidad de reutilización no es fácil de lograr; requiere más experiencia y conocimiento para diseñar una buena solución y hacer posible su reutilización.
Mucha gente considera que la reutilización de código o diseño es la mayor palanca que ofrece la programación orientada a objetos.
La forma más sencilla de reutilizar una clase es simplemente utilizar objetos de esa clase directamente. Pero también es posible colocar un objeto de esa clase en una nueva clase. A esto lo llamamos "crear un objeto miembro". La nueva clase puede estar compuesta por cualquier número y tipo de otros objetos. En cualquier caso, siempre y cuando la nueva clase cumpla con los requisitos de diseño. Este concepto se llama "organización": organizar una nueva clase basada en una clase existente. A veces, también nos referimos a las organizaciones como relaciones "que contienen", como "un automóvil contiene una caja de cambios".
Los objetos se organizan con gran flexibilidad. Los "objetos miembro" de una nueva clase generalmente se configuran como "Privados" (Privados) y los programadores cliente que usan esta clase no pueden acceder a ellos. De esta manera podemos modificar fácilmente esos miembros sin alterar el código del cliente. Los miembros también se pueden cambiar "en tiempo de ejecución", lo que aumenta aún más la flexibilidad. La "herencia", que se analizará más adelante, no tiene esta flexibilidad, porque el compilador debe imponer restricciones a las clases creadas mediante herencia.
Debido a la importancia de la herencia, a menudo se enfatiza en la programación orientada a objetos. Como nuevo programador que se incorpora a este campo, es posible que ya haya preconcebido la idea de que "la herencia debería verse en todas partes". El diseño producido según esta línea de pensamiento será muy torpe y aumentará en gran medida la complejidad del programa. Por el contrario, al crear una nueva clase, primero debes considerar "organizar" los objetos, esto es más simple y flexible; Utilizando la organización de objetos, nuestros diseños se mantienen nítidos. Esto se vuelve obvio una vez que necesitas usar la herencia.
1.5 Herencia: Reutilización de Interfaces
En sí mismo, el concepto de objetos puede aportarnos una gran comodidad. Conceptualmente nos permite encapsular varios datos y funciones juntos. De esta manera, el concepto de "espacio problemático" se puede expresar apropiadamente sin seguir deliberadamente la expresión de la máquina subyacente. En los lenguajes de programación, estos conceptos se reflejan como tipos de datos específicos (usando la palabra clave class).
Después de haber trabajado duro para crear un tipo de datos, sería muy frustrante si tuviéramos que crear un nuevo tipo para lograr aproximadamente la misma función. Pero si puede utilizar los tipos de datos existentes para "clonarlos" y luego agregarlos y modificarlos según la situación, la situación será mucho más ideal. "Herencia" está diseñada con este objetivo en mente. Pero la herencia no es exactamente equivalente a la clonación. Durante el proceso de herencia, si la clase original (formalmente llamada clase base, superclase o clase principal) cambia, la clase "clon" modificada (formalmente llamada clase o subclase heredada) también reflejará este cambio. En el lenguaje Java, la herencia se implementa mediante la palabra clave extends
Cuando se usa la herencia, equivale a crear una nueva clase. Esta nueva clase no sólo contiene todos los miembros del tipo existente (aunque los miembros privados están ocultos e inaccesibles), sino que, lo que es más importante, copia la interfaz de la clase base. Es decir, todos los mensajes que se pueden enviar a objetos de la clase base también se pueden enviar sin cambios a objetos de la clase derivada. En base a los mensajes que se pueden enviar, podemos saber el tipo de clase. ¡Esto significa que la clase derivada tiene el mismo tipo que la clase base! Para comprender verdaderamente lo que significa la programación orientada a objetos, primero debe reconocer este tipo de equivalencia.
Dado que la clase base y la clase derivada tienen la misma interfaz, esa interfaz debe estar especialmente diseñada. En otras palabras, después de que el objeto recibe un mensaje específico, debe haber un "método" que pueda ejecutarse. Si simplemente heredas una clase y no haces nada más, los métodos de la interfaz de la clase base se copiarán directamente a la clase derivada. Esto significa que los objetos de la clase derivada no sólo tienen el mismo tipo, sino que también tienen el mismo comportamiento, lo que suele ser una consecuencia indeseable.
Hay dos formas de distinguir la clase recién derivada de la clase base original.
El primer método es muy simple: agregar nuevas funciones a la clase derivada. Estas nuevas funciones no forman parte de la interfaz de la clase base. Al hacer esto, normalmente nos damos cuenta de que la clase base no puede cumplir con nuestros requisitos, por lo que necesitamos agregar más funciones. Este es el uso más simple y básico de la herencia, que puede resolver perfectamente nuestros problemas la mayor parte del tiempo. Sin embargo, debes investigar cuidadosamente de antemano si tu clase base realmente necesita estas funciones adicionales.
1.5.1 Mejora de las clases básicas
Aunque la palabra clave extends implica que queremos "extender" nuevas funciones para la interfaz, este no es necesariamente el caso. La segunda forma de diferenciar nuestra nueva clase es cambiar el comportamiento de una función existente en la clase base. A esto lo llamamos "mejorar" esa función.
Para mejorar una función, simplemente cree una nueva definición para la función en la clase derivada. Nuestro objetivo es: "Aunque la interfaz funcional utilizada permanece sin cambios, las nuevas versiones se comportan de manera diferente".
1.5.2 Equivalencia y similitud
Puede haber tal debate sobre la herencia: ¿Puede la herencia solo mejorar las funciones de la clase base original? Si la respuesta es sí, entonces el tipo derivado es exactamente el mismo tipo que la clase base porque ambas tienen exactamente la misma interfaz. El resultado de esto es: ¡podemos reemplazar completamente un objeto de la clase derivada con un objeto de la clase base! Piense en ello como un "puro reemplazo". En cierto sentido, esta es una forma ideal de heredar. En este punto, normalmente pensamos que existe una relación de "equivalencia" entre la clase base y la clase derivada, porque podemos decir con confianza: "Un círculo es una forma geométrica". Para probar la herencia, una forma es ver si se pueden encajar en esta relación de "equivalencia" y ver si tiene sentido.
Pero en muchos casos, debemos agregar nuevos elementos de interfaz a los tipos derivados. Así no sólo se amplía la interfaz, sino que también se crea un nuevo tipo. Este nuevo tipo aún se puede reemplazar por el tipo base, pero el reemplazo no es perfecto porque no se puede acceder a las nuevas funciones en la clase base. A esto lo llamamos relación de "similitud"; el nuevo tipo tiene la interfaz del tipo anterior, pero también contiene otras funciones, por lo que no se puede decir que sean completamente equivalentes. Como ejemplo, consideremos el caso de un frigorífico. Supongamos que nuestra habitación está conectada con varios controladores para la refrigeración, es decir, que ya tenemos las "interfaces" necesarias para controlar la refrigeración. Supongamos ahora que la máquina se estropea y la sustituimos por un nuevo aire acondicionado de frío y calor de doble función, que se puede utilizar tanto en invierno como en verano. Los acondicionadores de aire frío y caliente son "similares" a los refrigeradores, pero pueden hacer más. Dado que nuestra sala solo cuenta con equipos para controlar la refrigeración, se limitan a ocuparse de la parte de refrigeración de la nueva máquina. La interfaz de la nueva máquina se ha ampliado, pero el sistema existente no conoce nada más que la interfaz original.
Después de comprender la diferencia entre equivalente y similar, tendrá mucha más confianza al realizar sustituciones. Aunque el "reemplazo puro" es suficiente la mayor parte del tiempo, encontrará que, en algunos casos, todavía existen razones obvias para agregar nueva funcionalidad a la clase derivada. A través de la discusión anterior de estas dos situaciones, creo que todos ya saben qué hacer.
1.6 Uso intercambiable de objetos polimórficos
A menudo, la herencia acaba creando una serie de clases, todas ellas basadas en una interfaz unificada. Usamos un diagrama de árbol invertido para ilustrar este punto (nota ⑤):
⑤: Aquí se usa el "método de notación unificada", y este método se usará principalmente en este libro.
Para tal serie de clases, una cosa importante que debemos hacer es tratar el objeto de la clase derivada como un objeto de la clase base. Esto es importante porque significa que solo necesitamos escribir una única pieza de código que ignore los detalles específicos del tipo y solo trate con la clase base. De esta manera, ese código se puede separar de la información de tipo. Así es más fácil de escribir y de entender. Además, si se agrega un nuevo tipo mediante herencia, como "Triángulo", entonces el código que escribimos para el nuevo tipo "Geometría" funcionará tan bien como para el tipo anterior. Por tanto, el programa tiene "expansibilidad" y "expansibilidad".
Basado en el ejemplo anterior, supongamos que escribimos una función de este tipo en Java:
void doStuff(Shape s) {
s.erase() ;
// ...
s.draw();
}
Esta función se puede utilizar con cualquier "forma geométrica" (Forma) comunicación, por lo que es completamente independiente de cualquier tipo específico de objeto que esté dibujando (dibujando) y borrando (borrar). Si usamos la función doStuff() en algunos otros programas:
Círculo c = new Circle();
Triangle t = new Triangle();
Línea l = new Line();
doStuff;
doStuff;
doStuff;
Entonces la llamada a doStuff() Funciona bien automáticamente independientemente del tipo específico del objeto.
Esta es realmente una técnica de programación muy útil. Considere la siguiente línea de código:
hacerCosas;
En este punto, se pasa un identificador de Círculo a una función que esperaba un identificador de Forma. Dado que un círculo es una forma geométrica, doStuff() lo maneja correctamente. En otras palabras, cualquier mensaje que doStuff() pueda enviar a una Forma también puede ser recibido por Circle. Entonces esto es seguro y no causará errores.
A este proceso de tratar un tipo derivado como su tipo base lo llamamos "Upcasting". Entre ellos, "elegir" se refiere a la creación basada en un modelo listo para usar y "Arriba" indica que la dirección de herencia es desde "arriba", es decir, la clase base está en la parte superior y la clase derivada; se amplía a continuación. Por lo tanto, el modelado basado en la clase base es un proceso heredado de arriba, es decir, "Upcasting".
En los programas orientados a objetos se suele utilizar la tecnología de modelado upcast. Esta es una buena manera de evitar tener que investigar el tipo exacto. Eche un vistazo al código en doStuff():
s.erase();
// ...
s.draw();
p>
Ten en cuenta que no dice: "Si eres un Círculo, haz esto; si eres un Cuadrado, haz aquello; etc." Si escribe el código de esa manera, debe verificar todos los tipos posibles de Forma, como círculo, rectángulo, etc. Obviamente, esto es muy problemático y cada vez que se agrega un nuevo tipo de forma, debe modificarse en consecuencia. Aquí, simplemente decimos: "Eres una forma geométrica y sé que puedes borrarte a ti mismo, es decir, borrar(); por favor, realiza esa acción tú mismo y controla todos los detalles tú mismo".
1.6. 1 Enlace dinámico
En el código de doStuff(), lo más sorprendente es que aunque no dimos ninguna instrucción especial, la operación realizada fue completamente correcta y apropiada. Sabemos que el código ejecutado cuando se llama a draw() para un círculo es diferente del código ejecutado cuando se llama a draw() para un cuadrado o una línea. Pero al enviar un mensaje draw() a una Forma anónima, se tomará la acción correcta según el tipo real de identificador de Forma conectado en ese momento. Por supuesto, esto es sorprendente, porque cuando el compilador de Java compila el código para doStuff(), no sabe el tipo exacto en el que está operando. Si bien tenemos una garantía de que eventualmente se llamará a erase() para una Forma y a draw() para una Forma, no hay garantía de qué se llamará para un Círculo, Cuadrado o Línea específico.
Sin embargo, la acción final tomada también fue correcta. ¿Cómo se hizo?
Al enviar un mensaje a un objeto, si no se conoce el tipo específico de la otra parte, pero la acción realizada también es correcta, esta situación se denomina "Polimorfismo". Para los lenguajes de programación orientados a objetos, el método que utilizan para lograr el polimorfismo se denomina "enlace dinámico". El compilador y el sistema de ejecución se encargan de controlar todos los detalles, sólo necesitamos saber qué sucede y, lo que es más importante, cómo usarlo para ayudar a diseñar nuestros programas.
Algunos lenguajes requieren que usemos una palabra clave especial para permitir el enlace dinámico. En C, esta palabra clave es virtual. En Java, no tenemos que acordarnos de agregar ninguna palabra clave, porque el enlace dinámico de funciones se realiza automáticamente. Entonces, cuando enviamos un mensaje a un objeto, podemos estar absolutamente seguros de que el objeto realizará la acción correcta, incluso si implica un procesamiento como el upcasting.
1.6.2 Clases base abstractas e interfaces
Al diseñar un programa, a menudo esperamos que la clase base solo proporcione una interfaz para sus clases derivadas. Es decir, no queremos que nadie más cree un objeto de la clase base y solo lo actualice para usar su interfaz. Para lograr esto, debe hacer que esa clase sea "abstracta": use la palabra clave abstracta. Si alguien intenta crear un objeto de una clase abstracta, el compilador lo detendrá. Esta herramienta es efectiva para forzar un diseño particular.
La palabra clave abstracta también se puede usar para describir un método que aún no se ha implementado; se usa como "raíz" para indicar: "Esta es una función de interfaz que se aplica a todos los tipos que heredan de esta clase". , pero actualmente no existe ningún tipo de implementación del mismo. "Los métodos abstractos solo se pueden crear en una clase abstracta. Después de heredar una clase, ese método debe implementarse; de lo contrario, la clase heredada también se convertirá en una clase "abstracta". Al crear un método abstracto, podemos colocar un método en una interfaz sin tener que proporcionar un código corporal potencialmente sin sentido para ese método.
La palabra clave interface lleva el concepto de clases abstractas un paso más allá y prohíbe por completo todas las definiciones de funciones. "Interfaz" es una herramienta bastante eficaz y de uso común. Además, si lo desea, puede fusionar varias interfaces (no puede heredar de varias clases ordinarias o abstractas).
1.7 Tiempo de creación y existencia de objetos
Desde un punto de vista técnico, la POO (programación orientada a objetos) solo involucra tipos de datos abstractos, herencia y polimorfismo, pero puede haber otras cuestiones. también parecen ser muy importantes. Esta sección explorará estos temas.
Una de las cuestiones más importantes es cómo se crean y destruyen los objetos. ¿Dónde se encuentran los datos requeridos por el objeto y cómo controlar el "tiempo de existencia" del objeto? Hay varias soluciones a este problema. C considera que la eficiencia de ejecución del programa es el tema más importante, por lo que permite a los programadores tomar decisiones. Para obtener la velocidad de ejecución más rápida, el almacenamiento y la vida útil se pueden determinar al escribir el programa, simplemente coloque el objeto en la pila (a veces denominada variable automática o local) o en el área de almacenamiento estático. Esto proporciona una prioridad para la asignación y liberación de espacio de almacenamiento. En algunos casos, este tipo de control de prioridad puede resultar muy valioso. Sin embargo, también sacrificamos la flexibilidad porque al escribir un programa debemos saber el número, la edad y el tipo exactos de los objetos. Si el problema a resolver es un problema más rutinario, como el diseño asistido por computadora, la gestión de almacenes o el control del tráfico aéreo, este enfoque se vuelve demasiado limitante.
El segundo método consiste en crear objetos dinámicamente en un grupo de memoria, que también se denomina "montón" o "montón de memoria". Si usa este método, no sabrá cuántos objetos se necesitan, cuánto tiempo existen y cuáles son sus tipos exactos hasta que ingresa al tiempo de ejecución. Estos parámetros se determinan cuando el programa se ejecuta oficialmente. Si se necesita un nuevo objeto, simplemente se crea en el montón de memoria cuando sea necesario.
Dado que la gestión del espacio de almacenamiento se realiza dinámicamente durante el tiempo de ejecución, el tiempo para asignar espacio de almacenamiento en el montón de memoria es mucho más largo que el tiempo para crearlo en la pila (creando espacio de almacenamiento en la pila).