inheritance - relación - Mejores prácticas orientadas a objetos-Herencia v Composición v Interfaces
relación de composición java (12)
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.
Supongamos que estoy escribiendo una aplicación de videoclub cuya base de datos tiene una tabla Person
, con los PersonId
, Name
, DateOfBirth
y Address
. También tiene una tabla de Staff
, que tiene un enlace a una PersonId
, y una tabla de Customer
que también vincula a PersonId
.
Un enfoque simple orientado a objetos sería decir que un Customer
"es una" Person
y, por lo tanto, crear clases un poco como esta:
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 para enviar correos electrónicos 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 a la vez cliente y miembro del personal. Suponiendo que realmente no queremos que nuestra lista de everyone
tenga la misma persona en dos ocasiones, una como 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
.
Entonces, ¿qué harías?
- ¿La clase Hacer
Person
tiene referencias opcionales a una claseStaffDetails
yCustomerDetails
? - Crear una nueva clase que contenga una
Person
, además de losStaffDetails
yCustomerDetails
opcionales. - Hacer que todo sea una interfaz (por ejemplo,
IPerson
,IStaff
,ICustomer
) y crear tres clases que implementen las interfaces adecuadas. - ¿Tomar otro enfoque completamente diferente?
¿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 usar 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.
Aquí hay algunos consejos más: de la categoría "no piense en hacer esto", aquí hay algunos malos ejemplos de código encontrado:
El método Finder devuelve Object
Problema: Dependiendo de la cantidad de ocurrencias encontradas, el método de búsqueda devuelve un número que representa el número de ocurrencias - ¡o! Si solo uno encontrado devuelve el objeto real.
No hagas esto! Esta es una de las peores prácticas de codificación e introduce ambigüedad y altera 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, cree 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 mala práctica derivada es cuando un método de búsqueda devuelve la única ocurrencia encontrada, ya sea una matriz de ocurrencias si se encuentra más de una. Este estilo de programación perezosa se hace mucho por los programadores que hacen el anterior en general.
Solución: Teniendo esto en mis manos, devolvería una matriz de longitud 1 (uno) si solo se encuentra una ocurrencia y una matriz con longitud> 1 si se encuentran más ocurrencias. Además, al no encontrar ninguna ocurrencia, se devolverá nulo o una matriz de longitud 0 dependiendo de la aplicación.
Programación en una interfaz y uso de tipos de retorno covariantes
Problema: programación en una interfaz y uso de tipos de retorno covariantes y conversión en el código de llamada.
Solución: utilice en su lugar el mismo supertipo definido en la interfaz para definir 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 también representan un peligro!
Problema: Algunos desarrolladores incluyen demasiada funcionalidad en una clase / método, siendo demasiado perezosos para romper la funcionalidad, esto lleva a una baja cohesión y quizás a un alto acoplamiento, ¡lo contrario a 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 en: http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop
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. ¿Esto es 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)
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 son importantes para su API pública, por lo que puede elegir libremente lo que le haga la vida más simple.
Es posible que desee considerar el uso de los patrones de Parte y Responsabilidad
De esta manera, la persona tendrá un conjunto de responsabilidades que pueden ser de tipo Cliente o Personal.
El modelo también será más simple si agrega más tipos de relaciones más adelante.
Estudiamos este problema en la universidad el año pasado, estábamos aprendiendo eiffel, así que usamos herencia múltiple. De todos modos, la alternativa de los roles de Foredecker parece ser lo suficientemente flexible.
Evitaría el control "is" ("instanceof" en Java). Una solución es usar un patrón decorador . 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.
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.
Probablemente no desee usar 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; }
}
Sus clases son simplemente estructuras de datos: ninguna de ellas tiene ningún comportamiento, solo getters y setters. La herencia es inapropiada aquí.
Tome 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 un cliente más adelante, por lo que tendría que eliminarlos como personal y crear una nueva instancia de la clase StaffCustomer. Quizás un booleano simple dentro de la clase Staff de ''isCustomer'' permita que nuestra lista de todos (presumiblemente compilada para obtener todos los clientes y todo el personal de las tablas apropiadas) no obtenga al miembro del personal, ya que sabrá que ya se ha incluido como cliente.
Una Persona es un ser humano, mientras que un Cliente es solo un Papel 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 de Liskov dice que debemos ser capaces de usar clases derivadas en las que tengamos 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.