oop - object oriented programming
POO. Elegir objetos (10)
Dos cosas que pueden ser relevantes:
1) Neil Butterworth trae a colación una distinción muy importante entre un Modelo de Mundo Real en algún contexto versus el Mundo Real mismo. Diseño orientado a objetos a veces también denominado Modelado orientado a objetos por una razón. Un modelo solo se preocupa por los aspectos "interesantes" del mundo real. Lo que es interesante depende del contexto de su aplicación.
2) Favorecer la composición sobre la herencia . Una bebida tiene propiedades de volumen y temperatura (que en su contexto de modelado puede ser relativa a la percepción humana, es decir, muy fría, fría, cálida y caliente) y compuesta de ingredientes (agua, té, café, leche, azúcar, saborizantes). Tenga en cuenta que no hereda de los ingredientes, está hecho de ellos. En cuanto a las buenas maneras de componer ingredientes en bebidas, intente buscar el patrón decorador .
Soy relativamente nuevo para pensar en términos de OOP, y aún no he encontrado mi "instinto" en cuanto a la forma correcta de hacerlo. Como ejercicio, estoy tratando de averiguar dónde crearías la línea entre diferentes tipos de objetos, usando las bebidas en mi escritorio como ejemplo.
Asumiendo que creo un objeto Drink
, que tiene atributos como volume
y temperature
, y métodos como pour()
y drink()
, estoy luchando por ver dónde entran los ''tipos'' de bebida específicos.
Digamos que tengo un tipo de bebida de Tea
, Coffee
o Juice
, mi primer instinto es tomar una bebida subclase, ya que tienen atributos y métodos en común.
El problema se convierte en que tanto el Tea
como el Coffee
tienen atributos como sugars
y milk
, pero el Juice
no, mientras que los tres tienen una variant
(Earl Grey, decaff y orange respectivamente).
De manera similar, Tea
and Coffee
tiene un método addSugar()
, mientras que eso no tiene sentido para un objeto Juice
.
Entonces, ¿eso significa que la superclase debe tener esos atributos y métodos, incluso si todas las subclases no los necesitan, o los defino en las subclases, especialmente para atributos como la variant
, donde cada subclase tiene su propia lista de valores válidos?
Pero luego termino con dos métodos addSugar()
en las subclases Tea
y Coffee
.
O dado que termino poniendo todos los atributos y métodos en la superclase, ya que la mayoría se comparte entre al menos un par de tipos de bebida, me pregunto cuál fue el objetivo de subclasificar en absoluto.
Me temo que estoy tratando de abstraer demasiado, pero no quiero volver a arrinconarme si quisiera agregar un nuevo tipo, como Water
con la variant
fija o brillando en el camino.
El diseño orientado a objetos no se realiza de forma aislada. La pregunta que debe hacerse es: ¿qué necesita hacer mi aplicación específica (en todo caso) con una clase de bebida? La respuesta, y por lo tanto el diseño, diferirá de una aplicación a otra. La forma número 1 de fallar en OO es tratar de construir jerarquías platónicas de clases perfectas; tales cosas solo existen en mundos platónicos perfectos y no son útiles en el mundo real en el que vivimos.
Hay dos soluciones para el problema sugart. El primero es agregar una subclase como HotDrinks que contiene addSugar y addMilk como:
Drink
/ /
HotDrink Juice
/ /
Tea Coffee
El problema es que esto se complica si hay muchos de estos.
La otra solución es agregar interfaces. Cada clase puede implementar cero o más interfaces. Entonces usted tiene las interfaces ISweetened e ICowPowerEnabled. Donde tanto Tea como Coffee implementan la interfaz ISweetened y ICowPowerEnabled.
Para agregar a otras respuestas, las interfaces tienden a darle mayor flexibilidad para desacoplar el código.
Debe tener cuidado de no relacionar una característica con una clase base, solo porque es común a todas las clases derivadas. Piense más bien si esta característica se puede extraer y aplicar a otros objetos no relacionados: encontrará que muchos métodos en su clase base del "mundo real" en realidad podrían pertenecer a diferentes interfaces.
Por ejemplo, si está "agregando azúcar" a una bebida hoy, puede decidir agregarlo a un panqueque más tarde. Y luego puede terminar con su código pidiendo una bebida en muchos lugares, usando AddSugar y varios otros métodos no relacionados, puede ser difícil de refactorizar.
Si su clase sabe cómo hacer algo, entonces puede implementar la interfaz, independientemente de lo que la clase realmente sea. Por ejemplo:
interface IAcceptSugar
{
void AddSugar();
}
public class Tea : IAcceptSugar { ... }
public class Coffee : IAcceptSugar { ... }
public class Pancake : IAcceptSugar { ... }
public class SugarAddict : IAcceptSugar { ... }
El uso de una interfaz para solicitar una característica específica del objeto que realmente lo implementa lo ayuda a crear un código altamente independiente.
public void Sweeten(List<IAcceptSugar> items)
{
if (this.MustGetRidOfAllThisSugar)
{
// I want to get rid of my sugar, and
// frankly, I don''t care what you guys
// do with it
foreach(IAcceptSugar item in items)
item.AddSugar();
}
}
Tu código también se vuelve más fácil de probar. Cuanto más simple sea la interfaz, más fácil será probar a los consumidores de esa interfaz.
[Editar]
Quizás no estaba del todo claro, así que aclararé: si AddSugar () hace lo mismo para un grupo de objetos derivados, por supuesto lo implementarás en su clase base. Pero en este ejemplo, dijimos que Drink no siempre tiene que implementar IAcceptSugar.
Entonces podríamos terminar con una clase común:
public class DrinkWithSugar : Drink, IAcceptSugar { ... }
que implementaría AddSugar () de la misma manera para un grupo de bebidas.
Pero si un día se da cuenta de que este no es el mejor lugar para su método, no tendrá que cambiar otras partes del código, si solo está utilizando el método a través de la interfaz.
La moraleja es: su jerarquía puede cambiar, siempre y cuando sus interfaces permanezcan iguales.
Parte del problema es que los objetos del mundo real no están bien ordenados en una jerarquía. Cada objeto tiene muchos padres diferentes. Un vaso de jugo es ciertamente una bebida, pero también es una fuente de nutrición, pero no todas las bebidas lo son. No hay mucha nutrición en un vaso de agua. Del mismo modo, hay muchas fuentes de nutrición que no son bebidas. Pero la mayoría de los idiomas solo te permitirán heredar de una clase. Una taza de té es técnicamente una fuente de energía (está caliente, contiene energía térmica), pero también lo es una batería. ¿Tienen una clase base común?
Y, por supuesto, la pregunta extra, ¿qué me impide poner azúcar en mi jugo si quiero? Físicamente, eso es posible. Pero no está hecho convencionalmente. ¿Cuál quieres modelar?
En definitiva, no trates de modelar "el mundo". Modele su problema en su lugar. ¿Cómo quieres que tu aplicación trate una taza de té? ¿Cuál es el papel de una taza de té en su aplicación? ¿Cuál de los muchos posibles padres tiene sentido en tu caso?
Si su aplicación necesita distinguir entre "bebidas a las que puede agregar azúcar" y "bebidas que solo puede consumir sin tocar", entonces probablemente tenga esas dos clases base diferentes. ¿Incluso necesitas la clase común de "bebida"? Quizás, quizás no. A un bar puede no importarle que sea una bebida, que sea potable. es un producto que se puede vender, y en un caso, debe preguntar al cliente si quiere azúcar y, en el otro caso, eso no es necesario. Pero la clase base de "bebida" podría no ser necesaria.
Pero para la persona que bebe la bebida, es probable que desee tener una clase base "Bebida" con un método Consumir (), porque ese es el aspecto importante de la misma.
En última instancia, recuerde que su objetivo es escribir un programa que funcione y no escribir el diagrama de clase OOP perfecto. La pregunta no es "¿cómo puedo representar diferentes tipos de bebidas en OOP?", Sino "¿cómo puedo habilitar mi programa para tratar diferentes tipos de bebidas?". La respuesta podría ser organizarlos en una jerarquía de clases con una clase base común de Bebidas. Pero también podría ser algo completamente diferente. Podría ser "tratar todas las bebidas de la misma manera, y simplemente cambiar el nombre", en cuyo caso una clase es suficiente. O podría tratarse de bebidas simplemente como otro tipo de consumible, o simplemente otro tipo de líquido, sin realmente modelar su propiedad "potable".
Si está escribiendo un simulador de física, tal vez una batería y una taza de té provengan de la misma clase base, ya que la propiedad que le interesa es que ambas pueden almacenar energía. Y ahora el jugo y el té de repente no tienen nada en común. Ambos podrían ser bebidas en otro mundo, ¿pero en tu aplicación? Ellos son completamente distintos. (Y por favor, no me moleste sobre cómo el jugo contiene energía también. Hubiera dicho un vaso de agua, pero entonces alguien probablemente mencionaría el poder de fusión o algo así;))
Nunca pierdas de vista tu objetivo. ¿Por qué necesita modelar bebidas en su programa?
Si eres un novato, no me preocuparía si tu patrón de diseño no fuera perfecto. De hecho, muchos argumentarían que no existe un patrón de diseño perfecto: P
En cuanto a su ejemplo, probablemente crearía una clase abstracta para bebidas como el té y el café (porque comparten atributos similares). Y trate de hacer que la clase de bebida tenga muy pocos atributos y métodos. El agua y el jugo podrían subclasificarse a partir de la clase de bebida abstracta.
Use el patrón Decorator .
Todo depende de las circunstancias, y eso realmente significa, comenzar con el diseño más simple y refactorizarlo cuando veas la necesidad. Tal vez no se vea muy brillante OOP, pero comenzaría con una sola clase y una propiedad ''CanAddSugar'' y ''SugarAmount'' (más tarde se podría generalizar a un predicado CanAddIngredient (IngredientName) para que las personas puedan agregar whisky a sus bebidas: )
Hay una pregunta profunda en todo eso: ¿qué diferencia entre dos objetos es suficiente para requerir una clase separada para ellos? No he visto una buena respuesta a eso.
Un error común de los nuevos programadores de OO es no reconocer que la herencia se utiliza con dos propósitos diferentes, polimorfismo y reutilización. En los lenguajes de herencia únicos, normalmente tendrá que sacrificar la parte de reutilización y simplemente hacer polimorfismo.
Otro error es sobre el uso de la herencia. Si bien puede parecer una buena idea comenzar a diseñar una jerarquía de herencia hermosa, es mejor esperar hasta que surja un problema polimórfico y luego crear la herencia.
Si le dan un problema para crear clases para 4 tipos diferentes de bebidas, haga 4 tipos diferentes sin herencia. Si le dan un problema que dice "Ok, ahora queremos darle cualquiera de estas bebidas a una persona e imprimir lo que bebieron", ese es un uso perfecto del polimorfismo. Agregue una interfaz o clase base abstracta con una función de drink()
. Presione compilar, observe que las 5 clases dicen que la función no está implementada, implemente las 5 funciones, presione compilar y listo.
Para responder a su pregunta, su pregunta sobre la adición de leche o azúcar, el problema que supongo que está atravesando es que desea pasar un objeto que se pueda Drinkable
a una Person
pero addMilk
no tiene addMilk
o addSugar
. Este problema se puede resolver de dos o más maneras, una es una muy mala manera de hacerlo.
La mala manera: pasar un Drinkable
a una Person
y hacer una inspección de tipo y lanzar sobre el objeto para obtener un Tea
o Juice
.
void prepare (Drinkable drink)
{
if (drink is Juice)
{
Juice juice_drink = (Juick) drink;
juice_drink.thing();
}
}
Una forma: una forma es pasarle una bebida a la Person
través de clases concretas y luego pasarla a beber.
class Person
{
void prepare_juice (Juice drink)
{
drink.thing1();
}
void prepare_coffee (Coffee drink)
{
drink.addSugar ();
drink.addCream ();
}
}
Después de haberle pedido a la persona que prepare la bebida, luego pídales que la tomen.
Este pdf puede ayudarte mucho y encaja perfectamente con tu ejemplo: