2009-07-03 17 views
14

Soy relativamente novato en términos de POO, 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.OOP. Elección de objetos

Suponiendo puedo crear un objeto Drink, que tiene atributos como volume y temperature, y métodos como pour() y drink(), estoy luchando para ver dónde 'tipos' de bebida específica entran en juego.

Decir que tengo una bebida tipos de Tea, Coffee o Juice, mi primer instinto es la subclase Drink ya que tienen atributos y métodos en común.

El problema entonces se convierte en tanto Tea y Coffee tienen atributos como sugars y milk, pero Juice no lo hace, mientras que los tres tienen una variant (Earl Grey, descafeinado y naranja respectivamente).

Del mismo modo, Tea y Coffee tienen un método addSugar(), mientras que eso no tiene sentido para un objeto Juice.

Lo que 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 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 bebidas, me pregunto cuál fue el objetivo de subclasificar en absoluto?

Me temo que estoy tratando de abstraer demasiado, pero no quiero respaldarme en una esquina si quisiera agregar un nuevo tipo, como Water -con variant todavía o destellando en el camino.

+0

No es una respuesta directa a su pregunta, pero me pareció útil: aparte de los libros y fuentes de OOP, busque también * refactoring * material (por ejemplo, Refactorización de M. Fowler).Puede encontrar ejemplos concretos de códigos y acciones apropiadas para solucionarlos. Algunos ejemplos (Reemplazar condicional con polimorfismo, Generalizar tipo, etc.) le muestran algunas de las mejores prácticas simples en OOP, y pueden ayudar a reconocer el problema en el diseño en la fase inicial. – Groo

Respuesta

13

Parte del problema es que los objetos del mundo real no están ordenados ordenadamente 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 puede impedir poner azúcar en mi jugo si quiero? Físicamente, eso es posible. Pero no está hecho convencionalmente. ¿Cuál quieres modelar?

En última instancia, no intente modelar "el mundo". Modele su problema en su lugar. ¿Cómo desea que su aplicación trate con 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, puede agregar azúcar a" 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 para escribir el diagrama de clases de 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 para 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é deberían ser derivadas de la misma clase base, ya que la propiedad que le interesa es que ambas son capaces de almacenar energía. Y ahora el jugo y el té de repente 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, sin pensar en cómo el jugo contiene energía también. Hubiera dicho un vaso de agua, pero entonces alguien probablemente mencionaría la energía de fusión o algo así;))

Nunca pierda de vista su objetivo. ¿Por qué necesita modelar bebidas en su programa?

+0

Va a ir tomarse un tiempo para digerir esto, y las otras respuestas a mi pregunta original, además de leer un poco sobre los problemas planteados, así que no puedo decir que todos hayan resuelto el problema, pero puedo responder su última pregunta. Estoy planeando una aplicación para tomar pedidos de bebidas para una oficina, un sitio de construcción, etc., por lo que le preguntabas a la gente qué querían y al final terminaban con una lista de quién quería qué. Para que su pedido se vea como: Té, leche y 2 azúcares para Alice y Bob. Té, leche, sin azúcar para Charlie. Café, negro, un azúcar para Dean Jugo, naranja para Edward y Fiona. – creednmd

+0

Mi última pregunta fue para su propio beneficio, no para el mío. :) El punto es que debes decidir cómo modelar estos conceptos según tus propias necesidades, y no en un concepto universal de lo que "es" una bebida. Como dijo Neil, no se puede hacer una jerarquía de clases "platónica" perfecta. Todo lo que puede hacer es crear un modelo que se comporte de la manera que lo necesita. – jalf

+2

@ Los comentarios de Andy SO no son lugar para respuestas complejas, así que simplemente puedo sugerir que no parece necesitar una jerarquía de bebidas en absoluto. Necesitas una lista de personas y una cadena que describa la bebida que quieren. Esto es porque hay una variedad casi infinita de posibles clases de bebidas. –

13

Existen dos soluciones para el problema sugart. El primero es añadir una subclase como HotDrinks lo que se agradece contiene el addSugar y addMilk como:

    Drink 
       /\ 
      HotDrink Juice 
     / \ 
     Tea  Coffee 

El problema es que esto se complica si hay una gran cantidad 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.

+9

+1 para ICowPowerEnabled. Lol. – RCIX

17

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.

+0

Gran respuesta. +1 – jalf

+2

+1 para invocar a Platón. Si hubieras mencionado las paredes de las cuevas también, me habría vuelto loco. 8) – duffymo

+2

@duffymo Bueno, esto es por supuesto solo una sombra de mi respuesta real ... –

4

Este pdf que podría ayudar mucho y que encaja perfectamente con su ejemplo:

The Decorator Pattern

+1

@ Nathan W, edité el enlace para que la gente supiera lo que en realidad estás recomendando (iba a recomendar lo mismo) . Creo que es probable que obtengas más votos ahora :) –

+0

Genial, estaba preocupado de que iba a obtener votos para la respuesta del tipo "Aquí solo leí esto" –

1

Si usted es un novato, no me preocuparía por no conseguir su patrón de diseño 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.

+0

parece que el juego de gatos me ganó: P –

2

Dos cosas que podrían ser relevantes:

1) Neil Butterworth hace aparecer muy importante distinción entre un modelo de mundo real en un contexto real frente al mundo mismo. Diseño orientado a objetos a veces también denominado Modelado orientado a objetos por una razón. A model solo le preocupan los aspectos "interesantes" del mundo real. Lo que es interesante depende del contexto de su aplicación.

2) Favour composition over inheritance. Una bebida tiene propiedades de volumen y temperatura (que en su contexto de modelado puede ser relativa a la percepción humana, es decir, extra fría, fría, caliente 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 decorator pattern.

1

Uso Decorador patrón.

6

Para agregar a otras respuestas, las interfaces tienden a darle una mayor flexibilidad al 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á "la adición de azúcar" a una bebida hoy en día, es posible que decida 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 refactorizar.

Si su clase sabe cómo hacer algo, entonces puede implementar la interfaz, independientemente de qué clase es realmente. Por ejemplo:

interface IAcceptSugar 
{ 
    void AddSugar(); 
} 

public class Tea : IAcceptSugar { ... } 
public class Coffee : IAcceptSugar { ... } 
public class Pancake : IAcceptSugar { ... } 
public class SugarAddict : IAcceptSugar { ... } 

Usando una interfaz para solicitar una función específica del objeto que implementa realmente le ayuda a hacer 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(); 
    } 
} 

Su código también es 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]

Tal vez no era no completamente clara, así que voy a aclarar: si AddSugar() hace lo mismo cosa para un grupo de objetos derivados, que, por supuesto implementarlo en su clase base. Pero en este ejemplo, dijimos que Drink no siempre tiene que implementar IAcceptSugar.

Así que podríamos terminar con una clase común:

public class DrinkWithSugar : Drink, IAcceptSugar { ... } 

que implementaría AddSugar() de la misma manera para un montón de bebidas.

Pero si un día se da cuenta de que esto es no el mejor lugar para su método, no tendrá que cambiar otras partes de 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 las interfaces sean las mismas.

+1

+1 "consumidores de esa interfaz" es divertido en este contexto. – duffymo

+0

Tendrá que implementar el método AddSugar para cada bebida que implemente la interfaz cuando todas esas bebidas deberían tener defining AddSugar de base específica. Esto en la práctica te obligará a escribir muchos códigos de repitición. –

+0

Nunca tienes que duplicar el código. Solo su clase base necesita implementar AddSugar, como parte de la interfaz. Pero puede ocurrir que una de las clases derivadas se ajuste perfectamente a todos los métodos excepto a este. Luego puede crear fácilmente una nueva clase base (intermedia), mover algunos métodos allí y dejar su interfaz sin modificaciones para aquellos que saben cómo implementarla. – Groo

1

Un error común de los nuevos programadores de OO es no reconocer que la herencia se utiliza para 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 surge 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 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 pregunta sobre la adición de leche o azúcar el problema que estoy suponiendo que se está ejecutando en el que es que desea pasar un objeto a un DrinkablePerson pero Drinkable 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 una Drinkable a un Person y hacer una inspección tipo y yeso en el objeto de volver a Tea o Juice.

void prepare (Drinkable drink) 
{ 
    if (drink is Juice) 
    { 
    Juice juice_drink = (Juick) drink; 
    juice_drink.thing(); 
    } 
} 

Una manera: Una forma es hacer pasar una bebida a través de Person clases concretas y luego pasarlo a beber.

class Person 
{ 
    void prepare_juice (Juice drink) 
    { 
    drink.thing1(); 
    } 
    void prepare_coffee (Coffee drink) 
    { 
    drink.addSugar(); 
    drink.addCream(); 
    } 
} 

Después de que le haya pedido a la persona que prepare la bebida, les pedirá que la tomen.

0

Todo depende de las circunstancias, y eso realmente significa, comience con el diseño más simple y refactorícelo cuando vea 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.