2008-10-19 14 views
37

Quiero hacer una pregunta acerca de cómo abordaría un problema de diseño orientado a objetos simple. Tengo algunas ideas propias sobre cuál es la mejor manera de abordar este escenario, pero me gustaría escuchar algunas opiniones de la comunidad Stack Overflow. También se agradecen los enlaces a artículos relevantes en línea. Estoy usando C#, pero la pregunta no es específica del idioma.Mejores prácticas orientadas a objetos - Herencia v Composición v Interfaces

Supongamos que estoy escribiendo una aplicación de tienda de videos cuya base de datos tiene una mesa Person, con PersonId, Name, DateOfBirth y Address campos. También tiene una tabla Staff, que tiene un enlace a un PersonId, y una tabla Customer que también vincula a PersonId.

Un enfoque orientado a objetos simple sería decir que un Customer "es un" Person y por lo tanto crear clases un poco como esto:

class Person { 
    public int PersonId { get; set; } 
    public string Name { get; set; } 
    public DateTime DateOfBirth { get; set; } 
    public string Address { get; set; } 
} 

class Customer : Person { 
    public int CustomerId { get; set; } 
    public DateTime JoinedDate { get; set; } 
} 

class Staff : Person { 
    public int StaffId { get; set; } 
    public string JobTitle { get; set; } 
} 

Ahora podemos escribir una función dicen para enviar mensajes de correo electrónico a todos los clientes :

static void SendEmailToCustomers(IEnumerable<Person> everyone) { 
    foreach(Person p in everyone) 
     if(p is Customer) 
      SendEmail(p); 
} 

Este sistema funciona bien hasta que tengamos a alguien que sea tanto un cliente como un miembro del personal. Suponiendo que en realidad no quieren nuestra lista everyone tener la misma persona en dos veces, una vez como un Customer y otra como Staff, hacemos una elección arbitraria entre:

class StaffCustomer : Customer { ... 

y

class StaffCustomer : Staff { ... 

Obviamente, solo el primero de estos dos no rompería la función SendEmailToCustomers.

¿Qué harías?

  • hacer que la clase Person tienen referencias opcionales a una clase StaffDetails y CustomerDetails?
  • Crear una nueva clase que contenga un Person, más StaffDetails opcional y CustomerDetails?
  • Haga que todo sea una interfaz (por ejemplo, IPerson, IStaff, ICustomer) y cree tres clases que implementen las interfaces adecuadas.
  • ¿Adoptar otro enfoque completamente diferente?

Respuesta

47

Mark, esta es una pregunta interesante. Encontrarás tantas opiniones sobre esto. No creo que haya una respuesta "correcta". Este es un gran ejemplo de cómo un diseño de objeto jerárquico rígido realmente puede causar problemas después de que se construye un sistema.

Por ejemplo, digamos que fue con las clases "Cliente" y "Personal". Implementas tu sistema y todo está feliz.Unas semanas más tarde, alguien señala que ambos están "en el personal" y un "cliente" y no reciben correos electrónicos de los clientes. En este caso, tiene que hacer muchos cambios de código (rediseñar, no volver a factorizar).

Creo que sería demasiado complejo y difícil de mantener si intentas tener un conjunto de clases derivadas que implementen todas las permutaciones y combinaciones de personas y sus roles. Esto es especialmente cierto dado que el ejemplo anterior es muy simple: en la mayoría de las aplicaciones reales, las cosas serán más complejas.

Para su ejemplo aquí, yo iría con "Tome otro enfoque completamente diferente". Implementaría la clase Person e incluiría en ella una colección de "roles". Cada persona puede tener uno o más roles como "Cliente", "Personal" y "Proveedor".

Esto facilitará la adición de roles a medida que se descubran nuevos requisitos. Por ejemplo, puede simplemente tener una clase base "Role" y derivar nuevos roles de ellos.

10

El enfoque puro sería: Hacer que todo sea una interfaz. Como detalles de implementación, puede usar opcionalmente cualquiera de las diversas formas de composición o herencia de implementación. Dado que estos son detalles de implementación, no importan a su API pública, por lo que puede elegir libremente lo que le haga la vida más simple.

+0

Sí, y puede elegir una implementación ahora y cambiar de opinión más adelante sin romper otro código. –

16

Es posible que desee considerar el uso de la Party and Accountability patterns

De esta manera la persona tendrá una colección de Responsabilidades que puede ser del tipo de cliente o del personal.

El modelo también será más simple si agrega más tipos de relaciones más adelante.

1

Estudiamos este problema en la universidad el año pasado, estábamos aprendiendo eiffel, así que utilizamos herencia múltiple. De todos modos, la alternativa de los roles de Foredecker parece ser lo suficientemente flexible.

3

Evitaría la comprobación "is" ("instanceof" en Java). Una solución es usar un Decorator Pattern. Puede crear un EmailablePerson que decora Person donde EmailablePerson usa composition para contener una instancia privada de una Persona y delega todos los métodos que no sean de correo electrónico al objeto Person.

1

¿Qué hay de malo en enviar un correo electrónico a un cliente que es miembro del personal? Si él es un cliente, entonces se le puede enviar el correo electrónico. ¿Me equivoco al pensar eso? ¿Y por qué debería llevar a "todos" como su lista de correo electrónico? ¿No sería mejor tener una lista de clientes ya que estamos tratando con el método "sendEmailToCustomer" y no con el método "sendEmailToEveryone"? Incluso si desea utilizar la lista "todos", no puede permitir duplicados en esa lista.

Si ninguno de estos se puede lograr con una gran cantidad de redisgn, voy a ir con la primera respuesta de Foredecker y es posible que deba tener algunos roles asignados a cada persona.

+0

En el ejemplo dado, una Persona no puede ser tanto un cliente como un miembro del personal. De eso se trata la pregunta. – OregonGhost

+0

Hola, Creo que la pregunta es más sobre "No quiero enviar varios correos electrónicos si una persona es tanto cliente como miembro del personal". Para resolver este problema, 1) "Todos" no deberían permitir duplicados 2) Si permite duplicados, entonces la clase Persona debe tener "Roles" definidos como señaló Foredecker – vj01

5

Avísame si entendí la respuesta de Foredecker correctamente. Aquí está mi código (en Python, lo siento, no sé C#). La única diferencia es que no notificaría algo si una persona "es un cliente", lo haría si uno de sus papeles "está interesado" en esa cosa. ¿Es esto lo suficientemente flexible?

# --------- PERSON ---------------- 

class Person: 
    def __init__(self, personId, name, dateOfBirth, address): 
     self.personId = personId 
     self.name = name 
     self.dateOfBirth = dateOfBirth 
     self.address = address 
     self.roles = [] 

    def addRole(self, role): 
     self.roles.append(role) 

    def interestedIn(self, subject): 
     for role in self.roles: 
      if role.interestedIn(subject): 
       return True 
     return False 

    def sendEmail(self, email): 
     # send the email 
     print "Sent email to", self.name 

# --------- ROLE ---------------- 

NEW_DVDS = 1 
NEW_SCHEDULE = 2 

class Role: 
    def __init__(self): 
     self.interests = [] 

    def interestedIn(self, subject): 
     return subject in self.interests 

class CustomerRole(Role): 
    def __init__(self, customerId, joinedDate): 
     self.customerId = customerId 
     self.joinedDate = joinedDate 
     self.interests.append(NEW_DVDS) 

class StaffRole(Role): 
    def __init__(self, staffId, jobTitle): 
     self.staffId = staffId 
     self.jobTitle = jobTitle 
     self.interests.append(NEW_SCHEDULE) 

# --------- NOTIFY STUFF ---------------- 

def notifyNewDVDs(emailWithTitles): 
    for person in persons: 
     if person.interestedIn(NEW_DVDS): 
      person.sendEmail(emailWithTitles) 

+0

sí, parece una buena solución y es muy extensible. –

1

Sus clases son solo estructuras de datos: ninguna de ellas tiene ningún comportamiento, solo getters y setters. La herencia es inapropiada aquí.

7

Una persona es un ser humano, mientras que un cliente es simplemente un rol que una persona puede adoptar de vez en cuando. El hombre y la mujer serían candidatos para heredar Persona, pero el cliente es un concepto diferente.

El principio de sustitución Liskov dice que debemos ser capaces de utilizar las clases derivadas donde tenemos referencias a una clase base, sin saberlo. Tener el Cliente heredando Persona violaría esto. Un Cliente quizás también sea un papel desempeñado por una Organización.

+0

Una organización a menudo califica como un tipo de persona, es decir, una persona de la judicatura. – ProfK

1

Adopte otro enfoque completamente diferente: el problema con la clase StaffCustomer es que su miembro del personal podría comenzar como solo personal y convertirse en cliente más adelante, por lo que tendría que eliminarlos como personal y crear una nueva instancia de StaffCustomer clase. Tal vez un booleano simple dentro de la clase de personal 'isCustomer' permitiría a nuestra lista de todo el mundo (presumiblemente compilado de conseguir todos los clientes y todo el personal de las tablas apropiadas) de no conseguir el miembro del personal, ya que sabrá que se ha incluido ya como cliente.

1

Aquí hay más consejos: De la categoría de “ni siquiera pensar en hacer esto” aquí hay algunos malos ejemplos de código encontradas:

devuelve el método Localizador de objetos

Problema: En función del número de las ocurrencias encontradas, el método del buscador devuelve un número que representa el número de ocurrencias - ¡o! Si solo uno encontrado devuelve el objeto real.

No haga esto! Esta es una de las peores prácticas de codificación e introduce ambigüedad y se mete el código de una manera que cuando un desarrollador diferente entra en juego él o ella te odiará por hacer esto.

Solución: Si hay una necesidad de tales 2 funcionalidades: contar y recuperar una instancia, crea 2 métodos, uno que devuelva el recuento y otro que devuelva la instancia, pero nunca un solo método en ambos sentidos.

Problema: Una práctica mala derivado es cuando un método de búsqueda devuelve ya sea la una sola ocurrencia encontrar ya sea una matriz de ocurrencias si más de un conocer. Este estilo de programación perezosa se hace mucho por los programadores que hacen el anterior en general.

Solución: Tener esto en mis manos me volvería una matriz de longitud 1 (uno) si sólo se encuentra una ocurrencia y una matriz con longitud> 1 si más ocurrencias encontradas. Además, al no encontrar ninguna ocurrencia, se devolverá nulo o una matriz de longitud 0 dependiendo de la aplicación.

Programación de una interfaz y el uso de tipos de retorno covariantes

Problema: Programación de una interfaz y el uso de tipos de retorno covariantes y emitan en el código de llamada.

Solución: Utilizar en lugar del mismo supertipo definido en la interfaz para la definición de la variable que debe apuntar al valor devuelto. Esto mantiene la programación en un enfoque de interfaz y su código limpio.

Las clases con más de 1000 líneas son un peligro al acecho ¡Los métodos con más de 100 líneas son un peligro al acecho también!

Problema: Algunos desarrolladores incluyen demasiada funcionalidad en una clase/método, siendo demasiado perezosos para romper la funcionalidad, esto conduce a baja cohesión y tal vez a un alto acoplamiento, ¡lo contrario de un principio muy importante en OOP! Solución: Evite el uso de demasiadas clases internas/anidadas: estas clases se deben usar ÚNICAMENTE según cada necesidad, ¡no es necesario que las use! Usarlos podría generar más problemas, como limitar la herencia. ¡Cuidado con el código duplicado! El mismo o similar código podría existir en alguna implementación de supertipo o quizás en otra clase. Si está en otra clase que no es un supertipo, también violó la regla de cohesión. ¡Cuidado con los métodos estáticos, tal vez necesites una clase de utilidad para agregar!
Más información en: http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

0

Probablemente no desee utilizar la herencia para esto. Pruebe esto en su lugar:

class Person { 
    public int PersonId { get; set; } 
    public string Name { get; set; } 
    public DateTime DateOfBirth { get; set; } 
    public string Address { get; set; } 
} 

class Customer{ 
    public Person PersonInfo; 
    public int CustomerId { get; set; } 
    public DateTime JoinedDate { get; set; } 
} 

class Staff { 
    public Person PersonInfo; 
    public int StaffId { get; set; } 
    public string JobTitle { get; set; } 
} 
+2

¿Por qué? La respuesta sería más significativa si incluyera más de una explicación. Además, ¿cómo difiere esto de las respuestas existentes? – Leigh