Implementant associacions entre objectes

From Es1

Jump to: navigation, search

Contents

Característiques de les associacions

Navegabilitat: de quin a quin objecte he de poder accedir directament?

El que primer cal plantejar-se és de quin objecte a quin he de poder navegar. Això em dirà quin objecte té com a membre quin.

Quina direcció escollim? La que ens vagi millor per implementar les funcionalitats. Si un costat té propietat sobre l'altre (veure següent punt) el propietari hauria de tenir el membre

Navegabilitat.png

Lògicament, totes les associacions són bidireccionals; la navegabilitat és només un detall d'implementació. Però sovint ens agradaria que una associació fós navegable en tots dos sentits. Només cal posar als dos objectes relacionats una implementació unidireccional. La navegació bidireccional, però, és bastant més complexe de gestionar i si amb una direcció de navegació hi ha prou, no implementarem la segona.

A la pràctica de ES1 no haureu d'implementar bidireccionalitat, però, si un dia ho heu de fer, només cal posar membres d'associació unidireccional des de totes dues classes cap a l'altre. Ara bé, us aconsellem que la responsabilitat de establir o trencar la relació recaigui només sobre els mètodes d'una sola de les classes. D'aquesta manera evitareu codi duplicat i tindreu tot aquest aspecte de les classes centralitzat a un sol lloc.

Sabent aixó, a partir d'aqui, considerem totes les associacions unidireccionals.

Propietat: quin objecte es responsable d'alliberar un altre objecte?

Es diu que un objecte adquireix la propietat d'un altre quan esdevé el responsable de destruir-lo. En C++, definir clarament quina és la propietat de cada objecte és primordial per racionalitzar la gestió de memòria.

Hi ha dos casos en que el C++ gestiona automàticament la propietat (objectes automàtics):

  • els objectes que es creen a la pila (les variables locals no punters o referències), que es destrueixen quan l'execució surt del l'scope on s'han creat.
{
  Artista a;
} // Aqui es destrueix l'artista
  • els membres no punters o referències d'un objecte (membres automàtics, en endavant), que es destrueixen quan l'objecte que els conté es destrueix.

Només un objecte pot tenir la propietat d'un altre en un moment donat. La resta d'objectes poden estar associats però sense responsabilitat de gestió de memória.

Així doncs:

  • Si volem establir una relació que no impliqui propietat haurem de fer servir punters o referencies.
  • Sempre que puguem farem servir membres automàtics per associacions amb propietat.
  • Si volem propietat però, com veurem després, necessitem fer servir punters o referències, cal gestionar la propietat manualment (new, delete...).

Una altre aspecte important, però, que a la pràctica no veureu, és que quan un objecte es desfà d'un agregat seu en propietat, cal sempre eliminar totes les referències que qualsevol altre objecte del sistema guardi.

La propietat en C++ sovint es representa com a relació d'agregació forta (o composició) als diagrames UML estàtics de disseny. Les associacions sense propietat, són agregacions (dèbils) o simples associacions.

Propietat.png

Mutabilitat: L'objecte associat és el mateix durant tota la vida de l'objecte?

Si l'associació entre A i B dura tota la vida de A, la manera més simple i aconsellada d'implementar l'associació és:

  • Si A en té la propietat, fent B un membre normal de A, o,
  • Si no hi ha propietat, posant a A una referencia (&) cap a l'objecte B associat.

Ara bé, un membre normal sempre és el mateix i les referències tenen la limitació de no poder apuntar a un objecte diferent un cop inicialitzades. Així doncs, si aquesta relació ha de canviar, cal implementar-la amb punters.

Cal considerar tres coses si tenim propietat i mutabilitat:

Primer, és important deixar clar quins mètodes transfereixen propietat als paràmetres o al retorn per evitar errors:

  • Si ens transfereixen la responsabilitat i no la exercim tindrem memory leaks.
  • Si ens passen un objecte automàtic o propietat d'algu altre i nosaltres l'adoptem tindrem double free o accés a memòria alliberada.

Per convenció, quan hi hagi un traspás de propietat farem servir paràmetres o retorns punter. Quan simplement passem una referència sense propietat farem servir referències.

Track * nouTrack = compositor.composaTrack(reagee); // Transferint del compositor a la pila
repertori.afegeixTrack(new Track("Sole mio")); // Transferint de la pila a repertori
// En canvi
Track & track = cercaTrack("Sole mio"); // No transferim, es queda a repertori
album.afegeixTrack(track); // Tampoc transferim, l'album no gestion propietat

Segon, cal evitar la necessitat de transferències de propietat. Si la gestió de propietat no travessa la frontera de mòdul tot és més simple.

I tercer, cal destruir els objectes si són substituïts per d'altres.

Opcionalitat: pot l'objecte no estar associat?

A vegades les associacions són opcionals, és a dir, que donat un objecte de tipus A pot o no estar associat amb un del tipus B.

Aquest és un altre cas en que haurem de fer servir punters. Una referència sempre ha de apuntar a un lloc i un membre automàtic sempre hi és. En canvi, un punter pot ser nul perqué el podem fer valdre 0.

Ara bé, si es permet que sigui nul, cal gestionar aquest cas singular arreu. Per exemple, si te propietat, per alliberar-ho al destructor o quan canviem l'associació:

if (_membre) delete _membre;


Opcionalitat.png

Polimorfisme: es pot associar amb objectes de les subclasses?

Una altra situació on un membre automàtic no va prou bé és quan una relació és polimòrfica, és a dir, que hauríem de poder associar l'objecte amb objectes de qualsevol subclasse d'una classe base.

Per implementar el polimorfisme funcionen bé tant els punters com les referències. Farem servir les referències sempre que no hi hagi un altre factor (opcionalitat o mutabilitat) que ens obligui a fer servir un punter.

Polimorfisme.png

Multiplicitat: pot estar associat amb més d'un?

Una associació és múltiple en un sentit quan l'objecte que la implementa aquest sentit pot estar associat amb molts altres objectes.

La implementació d'una relació múltiple es fa normalment fent servir una estructura de dades de la STL com std::vector o std::list. Aquesta llista contindra:

  • objectes automatics, si te la propietat i no és polimorfica
  • punters gestionats, si te la propietat i és polimorfica
  • punters no gestionats, si no te la propietat


Multiplicitat.png

Resum d'implementacions possibles

Composició Simple
Composició Opcional
Composició Múltiple
Associació Simple
Associació Opcional
Associació Múltiple
Composició Simple Polimòrfica
Composició Opcional Polimòrfica
Composició Múltiple Polimòrfica
Associació Simple Polimòrfica
Associació Opcional Polimòrfica
Associació Múltiple Polimòrfica


Resumint la discussió anterior, si intentem escollir la implementació més senzilla per cada cas ens queda:

  • Membres automàtics (Propietat, 1:1, Immutable, No polimorfica)
  • Referencies (Sense propietat, 1:1, Immutable, Polimorfica)
  • Punters (Sense propietat, 0:1, Mutable, Polimorfica)
  • Punters gestionats (Amb propietat, 0:1, Mutable, Polimorfica)
  • Llistes a objectes (Propietat. 0:N, No polimorfica)
  • Llistes a punters d'objectes (Sense propietat, 0:N, Polimorfica)
  • Llistes a punters d'objectes gestionats (Propietat, 0:N, Polimorfica)

(Punters gestionats vol dir que cal fer deletes quan cal).

També podeu seguir el següent arbre de decissió:

  • No Múltiple?
    • Composició?
      • Opcional o Mutable?
        • -> Punters gestionats
      • Polimòrfic?
        • -> Punters gestionats
      • Else
        • -> Objectes normals
    • Associació o Agregació dèbil?
      • Opcional o mutable?
        • -> Pointer
      • Else
        • -> Reference
  • Múltiple?
    • Composició?
      • Polimòrfic?
        • -> Llista de punters gestionats
      • No polimòrfic?
        • -> Llista d'objectes
    • Associació o agregació dèbil?
      • -> Llista de punters

Activitats de reforç

  1. Segons això com implementaries els diagrames que apareixen a la dreta considerant que cap relació és mutable?
  2. Considera perque les altres implementacións no són viables o si ho són, perque són menys pràctiques? És important coneixer les viables no pràctiques perque ens serviran com a transició pel desenvolupament.
  3. Observa que les implementacions per Opcional i Mutable coincideixen al arbre de decissió. Realment serien estructures de dades iguals? També serien igual els mètodes?

Movent el disseny d'un tipus de relació a un altre amb TDD

Quan desenvolupem TDD, implementar de cop una relació complicada amb opcionalitat, multiplicitat o polimorfisme resulta sovint un pas massa gran per fer-ho de cop. Normalment, el que farem és fer evolucionar el codi d'una relació simple a una de més complicada afegint casos de tests. Veurem a la pràctica que les relacions són un element més del disseny que pot canviar per estratègia d'implementació o per canvi de requeriments. Dominar aquests mecanismes us treurà la por a evolucionar dissenys complicats.

Quan estiguem fent els test d'una agregació que ha d'esdevenir múltiple, es bona estratègia fer servir un mètode que retorni un string representant el que hi ha agregat.

Quan estiguem evolucionant el disseny d'una associació, és molt útil fer les transicions separant

  • l'estructura (el membre que la implementa),
  • els usos de construcció (codi que la modifica), i
  • els usos de consulta (codi que la fa servir sense modificar).

De fet, les transicions sovint es fan de la següent manera:

  • Duplicar: Sense eliminar la associació anterior, crear un nou membre requerit per la nova implementació. (Refactoring)
  • Replicar: On hi ha codi que modifica el membre antic, modificar també el nou, de forma que el nou membre tingui sempre les mateixes dades que el membre antic. (Refactoring)
  • Fallar i Substituir: Afegir i passar el test que força el canvi amb la implementació nova (Multiplicitat, opcionalitat, polimorfisme...). (RED+GREEN)
  • Iterar: Substituir, un a un passant els testos, els usos de consulta per que facin servir la implementació nova en comptes de l'antiga. (RED+GREEN)xN
  • Netejar: Eliminar els vestigis de l'estructura antiga (Refactoring)

D'aquesta forma sempre tindrem quelcom funcionant i els testos sempre us cobriran.

No Relació -> Agregació opcional

A la classe ClinicaVeterinaria volem afegir una agregació de pacients. De moment, nomes volem tenir Gats.

Partim de que les classes ClinicaVeterinaria i Gat ja existeixen. Aquí farem la seqüència: 0, 0..1, 0..N, tot i que a vegades convé fer la seqüència 1, 0..1, 0..N. Depen del codi.

Els passos que faríem serien:

  1. Test sense pacients
    1. Plantejem el test:
      • Creem una ClinicaVeterinaria 'clinica'
      • Cridem a un métode llistaDePacients()
      • Assertem que el que retorna es un string buit
    2. Per tenir el red, cal afegir aquest mètode i retornar un string amb brossa
    3. Per tenir el verd, retornem un string buit (o les capceleres de l'informe, sense contingut)
    4. Si l'informe que volem te capceleres amb informació de la clinica, refactoritzem introduint els camps d'informació real de la clínica a l'string.
  2. Test amb un pacient (Agregació opcional)
    1. Plantegem el test:
      • Creem una Clinicaveterinaria 'clinica'
      • Creem un Gat 'gat'
      • Cridem a clinica.afegeixPacient(gat)
      • Cridem a un métode llistaDePacients()
      • Assertem que el que retorna es un string amb l'entrada per aquest unic gat insertada
    2. Per obtenir el RED només cal afegir el mètode void afegeixPacient(Gat&gat) que no faci res
    3. La implementació tonta pel GREEN és:
      • Afegim un membre punter a Gat '_pacient'
      • Inicialitzem el punter a 0 en el constructor
      • En cridar el metode afegeixPacient() fer que el punter apunti al gat passat com paràmetre
        compte: el paràmetre ha de passar-se per referència
      • A llistaDePacients() fem el mateix si el punter és 0, sinó retornem literalment el text que esperem
    4. Refactor: composar el text de retorn amb les dades del gat

Agregació opcional -> Agregació múltiple

A la classe ClinicaVeterinaria tenim una agregacio opcional (dèbil, sense propietat) d'un o cap gat implementada amb un punter a un objecte Gat extern que pot ser null. Amb un sol pacient no farem negoci o sigui que volem tenir més d'un pacient.

El procediment passa per un refactoring de canvi d'estructura que permet canviar una estructura per una altre sense que en cap moment es deixin de passar els testos. Un refactoring de canvi d'estructura te les següents fases:

  1. Afegir la nova estructura sense esborrar la vella
    • En aquest cas afegirem una std::list<Gat*> anomenada '_gats'
    • Passem els testos i encara estem a green
  2. Modificar la nova estructura als mateixos llocs on es modificava la vella pero encara modificant la vella
    • A afegeixPacient, a més d'actualitzar '_gat' farem un push_back a '_gats'
    • Passem els testos i encara estem a green
  3. Plantegem el test:
    • Creem una Clinicaveterinaria 'clinica'
    • Creem gats Gats 'gat1' i 'gat2'
    • Afegim els dos gats
    • Assertem que el métode llistaDePacients() retorna la llista amb els dos gats
  4. RED: Passem el test, per tenir el RED
    • En executar els testos veuríem que només surt el darrer gat inserit
  5. GREEN: Substituim la implementació del mètodes que consulten l'estructura vella
    • A llistaDePacients() afegim una iteració per fer el que feiem amb un sol gat pero ara amb cada gat
    • A llistaDePacients() eliminem el que quedi el codi que feia servir el punter
    • Passem els testos i ara estem a GREEN
    • Per ser correctes caldria repetir el RED i el GREEN per cada mètode que consulti (no pas modifiqui) _gat.
  6. Refactor: Eliminem pas a pas el codi que modifica l'estructura antiga
    • A cada mètode netejat passem els testos i comprovem que estem a GREEN
    • Fent-ho progressivament és més facil trobar l'error quan els testos fallen o no compila
  7. Refactor: Eliminem l'estructura antiga
    • Eliminent el membre _gat
    • Passem els testos i ara estem a GREEN, si casca segurament és perque encara ens queden usos de _gat.

Agregació múltiple -> Composició múltiple

Tot i que els gats en el domini no són part de la clínica, en el sistema, tindrà sentit mantenir els gats pel fet de ser pacients de la clínica. És a dir, el nostre sistema no haurà de considerar gats que no siguin pacients de la nostra clínica. Així doncs, de cara a la implementació, té sentit donar propietat dels objectes gats a la clínica.

Fins ara, la propietat dels gats que creavem estava a la pila dels mètodes de test. Eren objectes automàtics que es destruien quan sortiem del mètode de test. Per exemple:

Clinica clinica;
...
Gat gat;
gat.setNom("mixu");
gat.setRaca(Gat::siames);
clinica.afegeixPacient(gat);
...

El que farem serà:

  • Crear un metode de traspàs de propietat (afegeixPacient(Gat*)) que fa el mateix que feia el mètode d'agregació (afegeixPacient(Gat&))
  • Canviar els usos (tests) per que facin servir aquest mètode de traspàs
clinica.afegeixPacient(&gat);
  • Canviar els usos (tests) per que crein els objectes al·locats en comptes de crear-los a la pila. Això provocara memory leaks.
Gat * gat = new Gat;
gat->setNom("mixu");
gat->setRaca(Gat::siames);
clinica.afegeixPacient(gat); // a voltes sense el '&'
  • Al destructor, fer els deletes de tots els objectes iterant per la llista. Això eliminarà els memory leaks. Si a algun lloc encara fem servir objectes de pila, el programa farà un segmentation fault.
~Clinica()
{
   for (Gats::iterator it=_gats.begin(); it!=_gats.end; it++) delete *it;
}
  • Afegir un mètode de creacio (afegeixPacient(parametres de creacio)) que faci new de l'objecte, posi els valors dels atributs i cridi el mètode de traspàs de propietat (estarem passant la propietat de la pila d'un mètode a la classe).
  • Fer servir als usos (tests) el mètode de creació en comptes de crear l'objecte al test.
  • (Opcional) Fer privat el mètode de traspàs o literalitzar-lo a dintre del mètode de creacio si no necessitem realment fer traspassos de propietat.

Seguir la gestió de memòria és més complicada si cal considerar traspassos de propietat. Si podem evitar els traspassos i centralitzar tota la gestió de la propietat dintre d'una sola classe, millor. Si no, cal tenir molt clar quins són els mètodes que traspassen propietat. La convenció dels punters és útil per explicitar-ho: Paràmetres i retorns punters indica traspàs de memòria, paràmetres i retorns referències indiquen no traspàs.

Agregació simple opcional/mutable -> Composició simple opcional/mutable

Si la agregació no és múltiple però és opcional o mutable (un sol punter) el procediment és el mateix que per a una múltiple pero cal tenir que en establir la conexió cal comprobar que si hi ha un objecte associat i alliberar-ho.

void afegeixPacient(Gat * gat)
{
   if (_gat) delete _gat;
   _gat = nouGat;
}

Agregació -> Agregació polimòrfica

A la classe ClinicaVeterinaria tenim una agregació múltiple (dèbil, sense propietat) de Gat i volem que sigui també de Tortuga sent Tortuga i Gat subclasses d'Animal.

La situació de partida hauria de ser que:

  • hem passat els testos per l'agregació múltiple de Gat,
  • tenim un membre std::list<Gat *> _gats; (estructura)
  • tenim una funció afegeixPacient(Gat & gat) per poblar l'agregació (modificació de l'estructura)
  • tenim una funció llistaDePacients() que ens dona els noms dels animals en un string (ús de l'estructura)

Els passos són:

  1. Fer els mateixos passos que vam fer per tenir l'agregació de Gats però amb una agregació de Tortugues, de tal forma que
    • la funció sobrecarregada afegeixPacient(Tortuga & tortuga) afegeix a un membre std::list<Tortuga *> _tortugues;
    • llistaDePacients() travessa les dues llistes
    • al final del procés tenim tests equivalents passant per tortugues i per gats.
  2. Sense esborrar les altres, creem una tercera estructura std::list<Animal *> _pacients;. Tot hauria de seguir funcionant.
  3. Als dos mètodes afegirPacient fem que, a més d'omplir la llista de _gats o _tortugues, també s'ompli la llista de _pacients. Tindrem una estructura duplicada.
  4. Un cop que totes les modificacions que es fan a les estructures _gats i _tortugues s'apliquen també a _pacients, podem anar substituint la resta dels usos de _gats i _tortugues perque facin servir _pacients. Això s'ha de fer d'una forma progressiva i passant els tests a cada substitució.
  5. Un cop que els usos estan tots redirigits cap a la nova estructura, ja podem anar eliminant poc a poc els llocs on es modifiquen les estructures antigues.
  6. Eliminem les estructures antigues.
  7. En aquest punt, la implementacio dels mètodes afegirPacient(Gat) i afegirPacient(Tortuga) haurien de ser iguals (afegir el que sigui a _pacients). Així doncs fem un refactoring per eliminar duplicació: ho colapsem en un mètode afegirPacient(Animal &animal)



TODO
Personal tools