Best practice orientate agli oggetti – Ereditarietà v Composizione v Interfacce

Voglio porre una domanda su come affrontate un semplice problema di progettazione orientato agli oggetti. Ho alcune idee personali su quale sia il modo migliore di affrontare questo scenario, ma sarei interessato ad ascoltare alcune opinioni della community di Stack Overflow. Sono anche apprezzati i link a articoli online pertinenti. Sto usando C #, ma la domanda non è specifica per la lingua.

Supponiamo che io PersonId scrivendo un’applicazione di video store il cui database ha una tabella Person , con PersonId , Name , DateOfBirth e Address . Ha anche una tabella Staff , che ha un collegamento a un PersonId e una tabella Customer che si collega anche a PersonId .

Un semplice approccio orientato agli oggetti sarebbe dire che un Customer “è una Person ” e quindi creare classi un po ‘come questo:

 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; } } 

Ora possiamo scrivere una funzione per inviare email a tutti i clienti:

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

Questo sistema funziona bene finché non abbiamo qualcuno che è sia un cliente sia un membro dello staff. Supponendo che non vogliamo davvero che la nostra lista di everyone abbia la stessa persona in due volte, una volta come Customer e una volta come Staff , facciamo una scelta arbitraria tra:

 class StaffCustomer : Customer { ... 

e

 class StaffCustomer : Staff { ... 

Ovviamente solo il primo di questi due non interromperebbe la funzione SendEmailToCustomers .

Quindi cosa faresti?

  • La class Person ha riferimenti facoltativi a una class StaffDetails e CustomerDetails ?
  • Creare una nuova class che contenga una Person , oltre a StaffDetails e CustomerDetails facoltativi?
  • Rendi tutto un’interfaccia (es. IStaff , IStaff , ICustomer ) e crea tre classi che implementano le interfacce appropriate?
  • Prendi un altro approccio completamente diverso?

Marco, questa è una domanda interessante. Troverete tante opinioni su questo. Non credo che ci sia una risposta “giusta”. Questo è un ottimo esempio di dove un rigido design di oggetti gerarchici può davvero causare problemi dopo la creazione di un sistema.

Ad esempio, diciamo che sei andato con le classi “Cliente” e “Personale”. Disponi il tuo sistema e tutto è felice. Qualche settimana dopo, qualcuno sottolinea che entrambi sono “sullo staff” e su un “cliente” e non ricevono le email dei clienti. In questo caso, è necessario apportare molte modifiche al codice (riprogettazione, non rifattorizzazione).

Credo che sarebbe eccessivamente complesso e difficile da mantenere se si tenta di avere un insieme di classi derivate che implementano tutte le permutazioni e le combinazioni di persone e dei loro ruoli. Questo è particolarmente vero dato che l’esempio sopra è molto semplice – nella maggior parte delle applicazioni reali, le cose saranno più complesse.

Per il tuo esempio qui, vorrei andare con “Prendi un approccio completamente diverso”. Implementerei la class Person e includerò una serie di “ruoli”. Ogni persona potrebbe avere uno o più ruoli come “Cliente”, “Personale” e “Fornitore”.

Ciò faciliterà l’aggiunta di ruoli man mano che vengono scoperti nuovi requisiti. Ad esempio, potresti semplicemente avere una class “Ruolo” di base e ricavarne nuovi ruoli.

Potresti prendere in considerazione l’ idea di utilizzare i modelli Party e Accountability

In questo modo, la persona avrà una raccolta di responsabilità che potrebbero essere di tipo Cliente o Personale.

Il modello sarà anche più semplice se aggiungerai più tipi di relazione in seguito.

L’approccio puro sarebbe: rendere tutto un’interfaccia. Come dettagli di implementazione, puoi facoltativamente utilizzare una qualsiasi delle varie forms di composizione o ereditarietà dell’implementazione. Dato che si tratta di dettagli di implementazione, non sono importanti per la tua API pubblica, quindi sei libero di scegliere quale ti semplifica la vita.

Una persona è un essere umano, mentre un cliente è solo un ruolo che una persona può adottare di volta in volta. L’uomo e la donna sarebbero candidati per ereditare la persona, ma il cliente è un concetto diverso.

Il principio di sostituzione di Liskov dice che dobbiamo essere in grado di usare classi derivate in cui abbiamo riferimenti a una class base, senza saperlo. Avere il Cliente ereditare Persona violerebbe questo. Un cliente potrebbe anche essere un ruolo giocato da un’organizzazione.

Fammi sapere se ho capito correttamente la risposta di Foredecker. Ecco il mio codice (in Python, mi dispiace, non so C #). L’unica differenza è che non informsrei qualcosa se una persona “è un cliente”, lo farei se uno dei suoi ruoli “è interessato a” quella cosa. È abbastanza flessibile?

 # --------- 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) 

Eviterei il controllo “is” (“instanceof” in Java). Una soluzione è usare un pattern Decorator . È ansible creare una persona disponibile per decorare la persona in cui EmailablePerson utilizza la composizione per conservare un’istanza privata di una persona e delega tutti i metodi non email all’object Person.

Studiamo questo problema al college l’anno scorso, stavamo imparando eiffel, quindi abbiamo usato l’ereditarietà multipla. In ogni caso, i ruoli di Foredecker sembrano essere abbastanza flessibili.

Cosa c’è di sbagliato nell’invio di un’e-mail a un cliente che è membro dello staff? Se è un cliente, quindi può essere inviato l’e-mail. Ho sbagliato a pensare così? E perché dovresti prendere “tutti” come tua mailing list? Non è meglio avere un elenco di clienti poiché abbiamo a che fare con il metodo “sendEmailToCustomer” e non con il metodo “sendEmailToEveryone”? Anche se si desidera utilizzare la lista “tutti”, non è ansible consentire duplicati in tale elenco.

Se nessuno di questi è realizzabile con un sacco di redisgn, andrò con la prima risposta di Foredecker e forse dovresti avere alcuni ruoli assegnati a ciascuna persona.

Le tue classi sono solo strutture dati: nessuno di loro ha alcun comportamento, solo getter e setter. L’ereditarietà è inappropriata qui.

Prendi un altro approccio completamente diverso: il problema con la class StaffCustomer è che il tuo membro dello staff potrebbe iniziare come personale e diventare un cliente in un secondo momento, quindi dovrai eliminarli come staff e creare una nuova istanza della class StaffCustomer. Forse un semplice booleano all’interno della class Staff di ‘isCustomer’ permetterebbe alla nostra lista di tutti (presumibilmente compilata di ottenere tutti i clienti e tutto il personale da tabelle appropriate) per non ottenere il membro dello staff in quanto saprà che è già stato incluso come cliente.

Ecco alcuni suggerimenti in più: dalla categoria “non pensare nemmeno a farlo” ecco alcuni esempi di codice errati:

Il metodo Finder restituisce Object

Problema: in base al numero di occorrenze rilevate, il metodo finder restituisce un numero che rappresenta il numero di occorrenze, oppure! Se solo uno trovato restituisce l’object reale.

Non farlo! Questa è una delle peggiori pratiche di codifica e introduce ambiguità e incasina il codice in modo tale che quando uno sviluppatore diverso entra in gioco lei o lui ti odierà per averlo fatto.

Soluzione: se c’è una necessità per queste 2 funzionalità: contare e recuperare un’istanza non creare due metodi uno che restituisce il conteggio e uno che restituisce l’istanza, ma mai un singolo metodo che esegue entrambi i modi.

Problema: una ctriggers pratica derivata è quando un metodo finder restituisce l’una singola occorrenza che trova una matrice di occorrenze se ne trova più di una. Questo pigro stile di programmazione è fatto molto dai programmatori che fanno il precedente in generale.

Soluzione: Avendo questo sulle mie mani restituirei un array di lunghezza 1 (uno) se viene trovata una sola occorrenza e un array con lunghezza> 1 se sono stati rilevati più casi. Inoltre, la ricerca di nessuna occorrenza restituisce null o una matrice di lunghezza 0 a seconda dell’applicazione.

Programmazione su un’interfaccia e utilizzo di tipi di ritorno covarianti

Problema: programmazione su un’interfaccia e utilizzo di tipi di ritorno covarianti e casting nel codice chiamante.

Soluzione: utilizzare invece lo stesso supertipo definito nell’interfaccia per definire la variabile che dovrebbe puntare al valore restituito. Ciò mantiene la programmazione in un approccio di interfaccia e il tuo codice pulito.

Le classi con più di 1000 linee sono un pericolo in agguato Anche i metodi con oltre 100 linee sono un pericolo in agguato!

Problema: alcuni sviluppatori fanno troppa funzionalità in una class / metodo, essendo troppo pigri per rompere la funzionalità – questo porta a una bassa coesione e forse a un elevato accoppiamento – l’inverso di un principio molto importante in OOP! Soluzione: Evita di usare troppe classi interne / nidificate – queste classi devono essere utilizzate SOLO per necessità, non devi abituarti a usarle! Usarli potrebbe portare a più problemi come limitare l’ereditarietà. Cerca il codice duplicato! Il codice stesso o troppo simile potrebbe già esistere in alcune implementazioni di supertipo o forse in un’altra class. Se si trova in un’altra class che non è un supertipo, hai anche violato la regola di coesione. Fai attenzione ai metodi statici – forse hai bisogno di una class di utilità da aggiungere!
Maggiori informazioni su: http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

Probabilmente non vuoi usare l’ereditarietà per questo. Prova questo invece:

 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; } }