Estoy tratando de entender los principios SÓLIDOS de OOP y he llegado a la conclusión de que LSP y OCP tienen algunas similitudes (si no decir más).
El principio abierto / cerrado establece que "las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para la modificación".
LSP en palabras simples establece que cualquier instancia de Foo
puede ser reemplazada por cualquier instancia de la Bar
cual se deriva Foo
y el programa funcionará de la misma manera.
No soy un programador profesional de OOP, pero me parece que el LSP solo es posible si Bar
, derivado de Foo
, no cambia nada en él, sino que solo lo extiende. Eso significa que, en particular, el programa LSP es verdadero solo cuando OCP es verdadero y OCP es verdadero solo si LSP es verdadero. Eso significa que son iguales.
Corrígeme si estoy equivocado. Realmente quiero entender estas ideas. Muchas gracias por una respuesta.
Square
deRectangle
no viola LSP. (Pero probablemente sigue siendo un mal diseño en el caso inmutable, ya que puede tener cuadradosRectangle
queSquare
no coinciden con las matemáticas)Respuestas:
Gosh, hay algunos conceptos erróneos extraños sobre qué OCP y LSP y algunos se deben a la falta de coincidencia de algunas terminologías y ejemplos confusos. Ambos principios son solo la "misma cosa" si los implementa de la misma manera. Los patrones generalmente siguen los principios de una forma u otra con pocas excepciones.
Las diferencias se explicarán más abajo, pero primero echemos un vistazo a los principios mismos:
Principio Abierto-Cerrado (OCP)
Según el tío Bob :
Tenga en cuenta que la palabra extender en este caso no significa necesariamente que deba subclasificar la clase real que necesita el nuevo comportamiento. ¿Ves cómo mencioné en la primera falta de coincidencia de terminología? La palabra clave
extend
solo significa subclases en Java, pero los principios son más antiguos que Java.El original vino de Bertrand Meyer en 1988:
Aquí es mucho más claro que el principio se aplica a las entidades de software . Un mal ejemplo sería anular la entidad de software ya que está modificando el código por completo en lugar de proporcionar algún punto de extensión. El comportamiento de la entidad de software en sí debería ser extensible y un buen ejemplo de esto es la implementación del patrón de estrategia (porque es el más fácil de mostrar del conjunto de patrones de GoF en mi humilde opinión):
En el ejemplo anterior, el
Context
está bloqueado para modificaciones adicionales. La mayoría de los programadores probablemente desearían subclasificar la clase para extenderla, pero aquí no lo hacemos porque supone que su comportamiento se puede cambiar a través de cualquier cosa que implemente laIBehavior
interfaz.Es decir, la clase de contexto está cerrada para modificación pero abierta para extensión . En realidad, sigue otro principio básico porque estamos poniendo el comportamiento con la composición del objeto en lugar de la herencia:
Dejaré que el lector lea sobre ese principio ya que está fuera del alcance de esta pregunta. Para continuar con el ejemplo, digamos que tenemos las siguientes implementaciones de la interfaz IBehavior:
Usando este patrón podemos modificar el comportamiento del contexto en tiempo de ejecución, a través del
setBehavior
método como punto de extensión.Por lo tanto, siempre que desee ampliar la clase de contexto "cerrado", hágalo subclasificando su dependencia colaboradora "abierta". Claramente, esto no es lo mismo que subclasificar el contexto en sí, pero es OCP. LSP tampoco menciona esto.
Extendiéndose con Mixins en lugar de Herencia
Hay otras formas de hacer OCP que no sean subclases. Una forma es mantener sus clases abiertas para la extensión mediante el uso de mixins . Esto es útil, por ejemplo, en lenguajes basados en prototipos más que en clases. La idea es enmendar un objeto dinámico con más métodos o atributos según sea necesario, en otras palabras, objetos que se mezclan o "mezclan" con otros objetos.
Aquí hay un ejemplo de JavaScript de un mixin que representa una plantilla HTML simple para anclas:
La idea es extender los objetos dinámicamente y la ventaja de esto es que los objetos pueden compartir métodos incluso si están en dominios completamente diferentes. En el caso anterior, puede crear fácilmente otros tipos de anclajes html extendiendo su implementación específica con
LinkMixin
.En términos de OCP, los "mixins" son extensiones. En el ejemplo anterior,
YoutubeLink
nuestra entidad de software está cerrada para modificaciones, pero abierta para extensiones mediante el uso de mixins. La jerarquía de objetos se aplana, lo que hace que sea imposible verificar los tipos. Sin embargo, esto no es realmente algo malo, y explicaré más adelante que buscar tipos es generalmente una mala idea y rompe la idea con polimorfismo.Tenga en cuenta que es posible hacer herencia múltiple con este método ya que la mayoría de las
extend
implementaciones pueden mezclar múltiples objetos:Lo único que debe tener en cuenta es no colisionar los nombres, es decir, los mixins definen el mismo nombre de algunos atributos o métodos, ya que se anularán. En mi humilde experiencia, esto no es un problema y si sucede, es una indicación de un diseño defectuoso.
Principio de sustitución de Liskov (LSP)
El tío Bob lo define simplemente por:
Este principio es antiguo, de hecho, la definición del tío Bob no diferencia los principios, ya que eso hace que LSP aún esté estrechamente relacionado con OCP por el hecho de que, en el ejemplo de Estrategia anterior, se usa el mismo supertipo (
IBehavior
). Así que veamos su definición original por Barbara Liskov y veamos si podemos encontrar algo más sobre este principio que se parezca a un teorema matemático:Hagamos caso omiso de esto por un tiempo, note que no menciona clases en absoluto. En JavaScript, puedes seguir LSP aunque no esté explícitamente basado en clases. Si su programa tiene una lista de al menos un par de objetos JavaScript que:
... entonces se considera que los objetos tienen el mismo "tipo" y realmente no importa para el programa. Esto es esencialmente polimorfismo . En sentido genérico; No debería necesitar saber el subtipo real si está utilizando su interfaz. OCP no dice nada explícito sobre esto. También señala un error de diseño que la mayoría de los programadores novatos hacen:
Cada vez que sienta la necesidad de verificar el subtipo de un objeto, lo más probable es que lo haga INCORRECTAMENTE.
De acuerdo, por lo que no podría ser mal todo el tiempo, pero si usted tiene la necesidad de hacer algo de comprobación de tipos con
instanceof
o enumeraciones, que podría estar haciendo el programa un poco más enrevesado por sí mismo de lo que debe ser. Pero este no es siempre el caso; Hacks rápidos y sucios para hacer que las cosas funcionen es una buena concesión para hacer en mi mente si la solución es lo suficientemente pequeña, y si practicas una refactorización despiadada , puede mejorar una vez que los cambios lo exijan.Hay formas de evitar este "error de diseño", dependiendo del problema real:
Ambos son "errores" comunes de diseño de código. Puede realizar un par de refactorizaciones diferentes, como el método pull-up o refactorizar un patrón como el patrón Visitor .
En realidad, me gusta mucho el patrón Visitor, ya que puede encargarse de los grandes espaguetis con sentencias if y es más sencillo de implementar de lo que pensarías en el código existente. Digamos que tenemos el siguiente contexto:
Los resultados de la declaración if se pueden traducir a sus propios visitantes, ya que cada uno depende de alguna decisión y algún código para ejecutar. Podemos extraer estos de esta manera:
En este punto, si el programador no sabía sobre el patrón Visitante, implementaría la clase Contexto para verificar si es de cierto tipo. Debido a que las clases Visitor tienen un
canDo
método booleano , el implementador puede usar esa llamada al método para determinar si es el objeto correcto para hacer el trabajo. La clase de contexto puede usar todos los visitantes (y agregar nuevos) así:Ambos patrones siguen OCP y LSP, sin embargo, ambos señalan cosas diferentes sobre ellos. Entonces, ¿cómo se ve el código si viola uno de los principios?
Violar un principio pero seguir el otro
Hay maneras de romper uno de los principios, pero aún se debe seguir el otro. Los ejemplos a continuación parecen inventados, por una buena razón, pero en realidad los he visto aparecer en el código de producción (e incluso peor):
Sigue OCP pero no LSP
Digamos que tenemos el código dado:
Este código sigue el principio abierto-cerrado. Si llamamos al
GetPersons
método del contexto , obtendremos un grupo de personas, todas con sus propias implementaciones. Eso significa que IPerson está cerrado por modificación, pero abierto por extensión. Sin embargo, las cosas se vuelven oscuras cuando tenemos que usarlo:¡Tienes que hacer la verificación de tipos y la conversión de tipos! ¿Recuerdas cómo mencioné anteriormente cómo la verificación de tipo es algo malo ? ¡Oh no! Pero no temas, como también se mencionó anteriormente, o bien realizas algunas refactorizaciones pull-up o implementas un patrón de visitante. En este caso, simplemente podemos hacer una refactorización pull-up después de agregar un método general:
El beneficio ahora es que ya no necesita saber el tipo exacto, siguiendo LSP:
Sigue LSP pero no OCP
Veamos un código que sigue a LSP pero no a OCP, es un poco artificial, pero tenga paciencia sobre este, es un error muy sutil:
El código hace LSP porque el contexto puede usar LiskovBase sin conocer el tipo real. Usted pensaría que este código también sigue a OCP, pero mire de cerca, ¿está realmente cerrada la clase ? ¿Qué pasa si el
doStuff
método hizo algo más que imprimir una línea?La respuesta si sigue a OCP es simplemente: NO , no es porque en este diseño de objeto se nos requiere anular el código por completo con otra cosa. Esto abre la lata de gusanos de cortar y pegar, ya que debe copiar el código de la clase base para que todo funcione. El
doStuff
método seguro está abierto para la extensión, pero no estaba completamente cerrado para la modificación.Podemos aplicar el patrón de método de plantilla en esto. El patrón del método de plantilla es tan común en los marcos que podría haberlo estado utilizando sin saberlo (por ejemplo, componentes de Java Swing, formularios y componentes de C #, etc.). Aquí hay una forma de cerrar el
doStuff
método de modificación y asegurarse de que permanezca cerrado marcándolo con lafinal
palabra clave de java . Esa palabra clave evita que cualquiera pueda subclasificar la clase aún más (en C # puede usarsealed
para hacer lo mismo).Este ejemplo sigue a OCP y parece una tontería, lo cual es, pero imagina que se amplió con más código para manejar. Sigo viendo el código implementado en la producción, donde las subclases anulan completamente todo y el código anulado se corta y pega principalmente entre implementaciones. Funciona, pero como con toda la duplicación de código, también es una configuración para las pesadillas de mantenimiento.
Conclusión
Espero que todo esto aclare algunas preguntas sobre OCP y LSP y las diferencias / similitudes entre ellos. Es fácil descartarlos como iguales, pero los ejemplos anteriores deberían mostrar que no lo son.
Tenga en cuenta que, reuniendo del código de muestra anterior:
OCP se trata de bloquear el código de trabajo pero aún así mantenerlo abierto de alguna manera con algún tipo de puntos de extensión.
Esto es para evitar la duplicación de código encapsulando el código que cambia como en el ejemplo del patrón de Método de plantilla. También permite fallar rápidamente, ya que los cambios importantes son dolorosos (es decir, cambiar un lugar, romperlo en cualquier otro lugar). En aras del mantenimiento, el concepto de encapsular el cambio es algo bueno, porque los cambios siempre ocurren.
LSP se trata de permitir que el usuario maneje diferentes objetos que implementan un supertipo sin verificar cuál es el tipo real. De esto se trata inherentemente el polimorfismo .
Este principio proporciona una alternativa para realizar la verificación de tipos y la conversión de tipos, que puede salirse de control a medida que aumenta el número de tipos y puede lograrse mediante la refactorización pull-up o la aplicación de patrones como Visitor.
fuente
Esto es algo que causa mucha confusión. Prefiero considerar estos principios de manera algo filosófica, porque hay muchos ejemplos diferentes para ellos, y a veces ejemplos concretos realmente no capturan toda su esencia.
Lo que OCP intenta arreglar
Digamos que necesitamos agregar funcionalidad a un programa dado. La forma más fácil de hacerlo, especialmente para las personas que fueron capacitadas para pensar de manera procesal, es agregar una cláusula if cuando sea necesario, o algo por el estilo.
Los problemas con eso son
Puede hacer esto agregando un campo adicional a todos los libros llamado "is_on_sale", y luego puede verificar ese campo al imprimir el precio de cualquier libro, o alternativamente , puede crear una instancia de libros en venta desde la base de datos usando un tipo diferente, que imprime "(EN VENTA)" en la cadena de precios (no es un diseño perfecto, pero entrega el punto a casa).
El problema con la primera solución de procedimiento es un campo adicional para cada libro y una complejidad adicional redundante en muchos casos. La segunda solución solo fuerza la lógica donde realmente se requiere.
Ahora considere el hecho de que podría haber muchos casos en los que se requieren diferentes datos y lógica, y verá por qué tener en cuenta OCP al diseñar sus clases, o reaccionar a los cambios en los requisitos, es una buena idea.
A estas alturas ya debe tener la idea principal: intente ponerse en una situación en la que se pueda implementar un nuevo código como extensiones polimórficas, no como modificaciones de procedimiento.
Pero nunca tenga miedo de analizar el contexto y ver si los inconvenientes superan los beneficios, porque incluso un principio como OCP puede hacer un desastre de clase 20 con un programa de 20 líneas, si no se trata con cuidado .
Lo que LSP intenta arreglar
Todos amamos la reutilización de códigos. Una enfermedad que sigue es que muchos programas no lo entienden completamente, hasta el punto de que están factorizando ciegamente líneas de código comunes solo para crear complejidades ilegibles y un acoplamiento estrecho redundante entre módulos que, aparte de unas pocas líneas de código, no tienen nada en común en lo que respecta al trabajo conceptual por hacer.
El mayor ejemplo de esto es la reutilización de la interfaz . Probablemente lo hayas presenciado tú mismo; una clase implementa una interfaz, no porque sea una implementación lógica de la misma (o una extensión en el caso de clases base concretas), sino porque los métodos que declara en ese punto tienen las firmas correctas en lo que respecta.
Pero entonces te encuentras con un problema. Si las clases implementan interfaces solo considerando las firmas de los métodos que declaran, entonces puede pasar instancias de clases de una funcionalidad conceptual a lugares que exigen una funcionalidad completamente diferente, que solo depende de firmas similares.
Eso no es tan horrible, pero causa mucha confusión, y tenemos la tecnología para evitar cometer errores como estos. Lo que debemos hacer es tratar las interfaces como API + Protocol . La API es evidente en las declaraciones, y el protocolo es evidente en los usos existentes de la interfaz. Si tenemos 2 protocolos conceptuales que comparten la misma API, deberían representarse como 2 interfaces diferentes. De lo contrario, quedamos atrapados en el dogmatismo SECO e, irónicamente, solo creamos códigos más difíciles de mantener.
Ahora deberías poder entender la definición perfectamente. LSP dice: No herede de una clase base e implemente la funcionalidad en esas subclases con las que, en otros lugares, que dependen de la clase base, no se llevarán bien.
fuente
Desde mi entendimiento:
OCP dice: "Si va a agregar una nueva funcionalidad, cree una nueva clase que amplíe una existente, en lugar de cambiarla".
LSP dice: "Si crea una nueva clase que amplía una clase existente, asegúrese de que sea completamente intercambiable con su base".
Así que creo que se complementan pero no son iguales.
fuente
Si bien es cierto que OCP y LSP tienen que ver con la modificación, el tipo de modificación del que habla OCP no es del que habla LSP.
La modificación con respecto a OCP es la acción física de un desarrollador que escribe código en una clase existente.
LSP se ocupa de la modificación del comportamiento que trae una clase derivada en comparación con su clase base, y la alteración del tiempo de ejecución de la ejecución del programa que puede ser causada por el uso de la subclase en lugar de la superclase.
Entonces, aunque podrían parecer similares desde la distancia, OCP! = LSP. De hecho, creo que pueden ser los únicos 2 principios SÓLIDOS que no pueden entenderse entre sí.
fuente
Esto está mal. LSP establece que la clase Bar no debería introducir un comportamiento, eso no se espera cuando el código usa Foo, cuando Bar se deriva de Foo. No tiene nada que ver con la pérdida de funcionalidad. Puede eliminar la funcionalidad, pero solo cuando el código que usa Foo no depende de esta funcionalidad.
Pero al final, esto suele ser difícil de lograr, porque la mayoría de las veces, el código que usa Foo depende de todo su comportamiento. Eliminarlo viola el LSP. Pero simplificarlo así es solo una parte de LSP.
fuente
Sobre objetos que pueden violar
Para comprender la diferencia, debe comprender temas de ambos principios. No es una parte abstracta de código o situación que puede violar o no algún principio. Siempre es algún componente específico (función, clase o módulo) el que puede violar OCP o LSP.
Quién puede violar LSP
Uno puede verificar si LSP está roto solo cuando hay una interfaz con algún contrato y una implementación de esa interfaz. Si la implementación no se ajusta a la interfaz o, en términos generales, al contrato, entonces LSP está roto.
El ejemplo más simple:
El contrato establece claramente que
addObject
debe agregar su argumento al contenedor. YCustomContainer
claramente rompe ese contrato. Por lo tanto, laCustomContainer.addObject
función viola LSP. Así, laCustomContainer
clase viola LSP. La consecuencia más importante es queCustomContainer
no se puede pasar afillWithRandomNumbers()
.Container
no puede ser sustituido conCustomContainer
.Tenga en cuenta un punto muy importante. No es todo este código el que rompe el LSP, es específicamente
CustomContainer.addObject
y generalmente elCustomContainer
que rompe el LSP. Cuando declara que se viola el LSP, siempre debe especificar dos cosas:Eso es. Solo un contrato y su implementación. Un downcast en el código no dice nada sobre la violación de LSP.
Quién puede violar OCP
Uno puede verificar si se viola OCP solo cuando hay un conjunto de datos limitado y un componente que maneja los valores de ese conjunto de datos. Si los límites del conjunto de datos pueden cambiar con el tiempo y eso requiere cambiar el código fuente del componente, entonces el componente viola OCP.
Suena complejo Probemos un ejemplo simple:
El conjunto de datos es el conjunto de plataformas compatibles.
PlatformDescriber
es el componente que maneja los valores de ese conjunto de datos. Agregar una nueva plataforma requiere actualizar el código fuente dePlatformDescriber
. Por lo tanto, laPlatformDescriber
clase viola OCP.Otro ejemplo:
El "conjunto de datos" es el conjunto de canales donde se debe agregar una entrada de registro.
Logger
es el componente responsable de agregar entradas a todos los canales. Agregar soporte para otra forma de inicio de sesión requiere actualizar el código fuente deLogger
. Por lo tanto, laLogger
clase viola OCP.Tenga en cuenta que en ambos ejemplos el conjunto de datos no es algo semánticamente fijo. Puede cambiar con el tiempo. Puede surgir una nueva plataforma. Puede surgir un nuevo canal de registro. Si su componente debe actualizarse cuando eso sucede, viola OCP.
Empujando los limites
Ahora la parte difícil. Compare los ejemplos anteriores con los siguientes:
Puede pensar que
translateToRussian
viola OCP. Pero en realidad no lo es.GregorianWeekDay
tiene un límite específico de exactamente 7 días de la semana con nombres exactos. Y lo importante es que estos límites semánticamente no pueden cambiar con el tiempo. Siempre habrá 7 días en la semana gregoriana. Siempre habrá lunes, martes, etc. Este conjunto de datos se arregla semánticamente. No es posible quetranslateToRussian
el código fuente requiera modificaciones. Por lo tanto, OCP no se viola.Ahora debe quedar claro que una
switch
declaración agotadora no siempre es una indicación de OCP roto.La diferencia
Ahora siente la diferencia:
Estas condiciones son completamente ortogonales.
Ejemplos
En @ respuesta de Spoike la violación de un principio, pero después de la otra parte es totalmente equivocado.
En el primer ejemplo, la
for
parte -loop está violando claramente el OCP porque no es extensible sin modificación. Pero no hay indicios de violación de LSP. Y ni siquiera está claro si elContext
contrato permite que getPersons devuelva algo exceptoBoss
oPeon
. Incluso suponiendo un contrato que permita laIPerson
devolución de cualquier subclase, no existe una clase que anule esta condición posterior y la viole. Además, si getPersons devolverá una instancia de alguna tercera clase,for
-loop hará su trabajo sin ningún fallo. Pero ese hecho no tiene nada que ver con LSP.Próximo. En el segundo ejemplo, no se viola ni LSP ni OCP. Una vez más, la
Context
parte simplemente no tiene nada que ver con LSP: sin contrato definido, sin subclases, sin anulaciones de ruptura. No esContext
quien debe obedecer a LSP, noLiskovSub
debe romper el contrato de su base. En cuanto a OCP, ¿ está realmente cerrada la clase? - sí lo es. No se necesita ninguna modificación para extenderlo. Obviamente, el nombre del punto de extensión indica Haz lo que quieras, sin límites . El ejemplo no es muy útil en la vida real, pero claramente no viola OCP.Tratemos de hacer algunos ejemplos correctos con verdadera violación de OCP o LSP.
Siga OCP pero no LSP
Aquí,
HumanReadablePlatformSerializer
no requiere ninguna modificación cuando se agrega una nueva plataforma. Por lo tanto, sigue OCP.Pero el contrato requiere que
toJson
debe devolver un JSON formateado correctamente. La clase no hace eso. Debido a eso, no se puede pasar a un componente que se utilizaPlatformSerializer
para formatear el cuerpo de una solicitud de red. Por lo tanto,HumanReadablePlatformSerializer
viola LSP.Siga LSP pero no OCP
Algunas modificaciones al ejemplo anterior:
El serializador devuelve una cadena JSON formateada correctamente. Entonces, no hay violación de LSP aquí.
Pero existe el requisito de que si la plataforma se usa en gran medida, debería haber una indicación correspondiente en JSON. En este ejemplo, la
HumanReadablePlatformSerializer.isMostPopular
función viola OCP porque algún día iOS se convirtió en la plataforma más popular. Formalmente significa que el conjunto de plataformas más utilizadas se define como "Android" por ahora, yisMostPopular
maneja de manera inadecuada ese conjunto de datos. El conjunto de datos no está semánticamente fijo y puede cambiar libremente con el tiempo.HumanReadablePlatformSerializer
Se requiere actualizar el código fuente en caso de un cambio.También puede notar una violación de la responsabilidad individual en este ejemplo. Lo hice intencionalmente para poder demostrar ambos principios sobre la misma entidad sujeto. Para arreglar SRP, puede extraer la
isMostPopular
función a alguna externaHelper
y agregarle un parámetroPlatformSerializer.toJson
. Pero esa es otra historia.fuente
LSP y OCP no son lo mismo.
LSP habla sobre la corrección del programa tal como está . Si una instancia de un subtipo rompería la corrección del programa cuando se sustituye en el código por tipos de antepasados, entonces usted ha demostrado una violación de LSP. Es posible que deba simular una prueba para mostrar esto, pero no tendría que cambiar la base de código subyacente. Está validando el programa en sí para ver si cumple con LSP.
OCP habla sobre la corrección de los cambios en el código del programa, el delta de una versión de origen a otra. El comportamiento no debe modificarse. Solo debe extenderse. El ejemplo clásico es la suma de campo. Todos los campos existentes continúan funcionando como antes. El nuevo campo solo agrega funcionalidad. Sin embargo, eliminar un campo suele ser una violación de OCP. Aquí está validando el delta de la versión del programa para ver si cumple con OCP.
Esa es la diferencia clave entre LSP y OCP. El primero valida solo la base del código tal como está , el segundo valida solo el delta base del código de una versión a la siguiente . Como tales, no pueden ser lo mismo, se definen como validar cosas diferentes.
Te daré una prueba más formal: decir "LSP implica OCP" implicaría un delta (porque OCP requiere uno distinto al caso trivial), pero LSP no requiere uno. Entonces eso es claramente falso. Por el contrario, podemos refutar "OCP implica LSP" simplemente diciendo que OCP es una declaración sobre deltas, por lo tanto, no dice nada acerca de una declaración sobre un programa en el lugar. Esto se deduce del hecho de que puede crear CUALQUIER delta comenzando con CUALQUIER programa establecido. Son totalmente independientes.
fuente
Lo miraría desde el punto de vista del cliente. si el Cliente está usando características de una interfaz, e internamente esa característica ha sido implementada por la Clase A. Supongamos que hay una clase B que extiende la clase A, luego mañana si elimino la clase A de esa interfaz y agrego la clase B, entonces la clase B debería También proporcionan las mismas características al cliente. El ejemplo estándar es una clase Duck que nada, y si ToyDuck extiende Duck, entonces también debe nadar y no se queja de que no puede nadar, de lo contrario ToyDuck no debería haber extendido la clase Duck.
fuente