Ocultació i Encapsulament en C ++
Tots hem sentit a parlar de l’encapsulació d’informació en els llenguatges orientats a objectes i en C ++. Anem a veure aquí en què consisteix i alguns “trucs” que podem fer en el cas concret de C ++ i que no solen venir en els llibres d’aquest llenguatge (encara que sí en llibres sobre patrons de disseny).
Els punts que veurem són:
- Encapsulament de losatributos deuna classe
- Importància de laencapsulaciónal compilar en C ++
- Encapsulament través d’interfícies
encapsulament dels atributs d’una classe
Abans de res, ha de quedar clar que l’encapsulament, igual que qualsevol bon hàbit de programació (com no posar goto , comentar, etc.) és útil per codi que més endavant es pot voler reutilitzar o modificar, per altres persones o per un mateix. Si jo faig un programa de marcians i mai mai penso tornar a tocar-lo, tant és que ho faci amb gotos i sense comentar mentre em n’assabenti jo mateix mentre ho estic fent i funcioni. Pagaré aquest “pecat” si d’aquí a dos mesos se m’acut millorar-lo o vull reaprofitar alguna cosa del seu codi per a un altre programa.
Comento això perquè l’encapsulament, portat al seu extrem, com és el cas de el punt final d’interfícies, fa la programació una mica més complicada (cal fer més classes). Aquest esforç només es veu recompensat si el codi és molt gran (evitant recompilats innecessaris) o es va a reutilitzar en un futur (podrem extreure classes amb menys dependències d’altres classes). Dit això, anem a el tema.
Qualsevol curs d’orientació a objectes ens diu que és millor posar els atributs d’una classe protegits o privats (mai públics) i accedir-hi a través de mètodes públics que posem a la classe. Vegem el motiu. Suposem, per exemple, que ens demanen un programa que permeti portar una llista de gent amb les seves dates de naixement. Entre altres coses, vam decidir fer-nos la nostra classe Data amb diversos mètodes meravellosos de la següent manera.
{
public: amor int anho; // El anho amb quatre xifres, ex. 2004 a int mes; // El mes, d’1 a 12 de int dia; // El dia, d’1 a 31 de void metodoMaravilloso1 (); a void metodoMaravilloso2 (); a};
Ja hem fet la classe. Ara fem la resta de el codi i en uns diversos milers de línies de codi usem directament coses com aquesta.
unaFecha.anho = 2004;
unaFecha.mes = 1;
unaFecha.dia = 25;
Finalment acabem el nostre programa i tot funciona de meravella. Uns dies després ens diuen que el programa va a guardar tropecientas mil persones i que ocupen molt els fitxers, que a veure si podem fer alguna cosa per posar-hi remei. Vaja !, emmagatzemem una data amb tres enters. Si fem servir el format de la majoria dels ordinadors, en el qual la data és el nombre de segons transcorreguts des de l’1 de gener de 1970 (el que ens retorna la funció time ()), n’hi ha prou amb un sencer.
Total, que mans a l’obra, vam canviar la nostra classe perquè tingui el següent:
{
public: a / * Comentat porineficiente
intanho; a intmes; a intdia; * /
longnumeroSegundos;
voidmetodoMaravilloso1 (); a void metodoMaravilloso2 (); a};
Ja està fet el fàcil. Ara només cal anar per les tropecientas mil línies de codi canviant les nostres assignacions i lectures als tres sencers anteriors pel nou long.
Hauria estat molt millor si haguéssim fet aquests tres sencers protegits i uns mètodes per accedir-hi. Una cosa com això
{
public: amor void tomaFecha (int anho, int mes, intdia); a int dameAnho (); a int dameMes (); a int dameDia (); a void metodoMaravilloso1 (); a void metodoMaravilloso2 ();
protected: amor int anho; // El anho amb quatre xifres, ex. 2004 a int mes; // El mes, d’1 a 12 de int dia; // El dia, d’1 a 31 de};
Si ara hem de fer el mateix canvi, només cal canviar els atributs protegits. Els mètodes tomaXXX () i dameXXX () es mantenen pel que fa a paràmetres i valor retornat, però es modifica el seu codi intern perquè converteixin l’any, mes i dia en un long de segons i a l’inrevés. La resta de el codi no cal tocar-lo en absolut.
És fins i tot millor fer els atributs privats que protegits. Fent-los protegits, les classes filles (les que hereten de Data) poden accedir directament a aquests atributs. Quan fem el canvi per un llarg, hem de canviar també el codi de les classes filles. Si els atributs són privats i obliguem a les classes filles a accedir-hi a través de mètodes, tampoc haurem de canviar el codi d’aquestes classes filles.
L’accés a través de mètodes és menys eficient que fer-ho directament, així que tot i que seguint el principi d’ocultació és millor fer atributs privats, per eficiència en alguns casos potser sigui millor fer-los protegits (o fins i tot públics) a risc d’haver de canviar més línies de codi en cas de canvi.
Importància de la encapsulació en C ++
Amb l’explicat fins ara evitem haver de canviar codi en cas de canviar paràmetres.
En el cas concret de C ++ hi ha un petit problema addicional. És bastant normal fer que les classes es defineixin per mitjà de dos fitxers. En el cas de la classe Data tindríem un Fecha.h amb la definició de la classe i un Fecha.cc (o .cpp) amb el codi dels mètodes de la classe. Quan volem fer servir la classe Data, solem fer el nostre #include < Fecha.h >.
Qualsevol procés de compilat eficient (com la utilitat make de linux i suposo que el Visual C ++) és prou llest com per recompilar només aquells fitxers que cal recompilar. És a dir, si ja tenim el nostre projecte compilat i toquem un fitxer, el compilador només compilarà aquest fitxer i tots els que en depenen. Aquesta característiques és molt important en projectes grans (amb molts fitxers i moltes línies de codi), per estalviar temps de compilat cada vegada que fem una modificació (He treballat en projectes que trigaven a compilar des de zero al voltant de 4 hores).
¿Quin és el problema ?. El problema és que si decidim, per exemple, canviar novament l’atribut privat de la classe Data per una altra cosa, necessitem tocar el fitxer Fecha.h. Això farà que es recompilen tots els fitxers que facin #include < Fecha.h > i tots els fitxers que facin #include d’algun fitxer que al seu torn faci #include de Fecha.hy així successivament.
La solució és evident, col·locar el menys possible en el fitxer Fecha.h, en concret els #define i variables globals que no sigui necessari veure des d’altres classes.
Per exemple, la nostra classe Data podia tenir uns #define per indicar quin és el nombre mínim i màxim de mes. És millor posar aquests #define en Fecha.cc en comptes d’en Fecha.h, llevat que algú hagi de veure’ls.
Encapsulament a través d’interfícies
Ens queda una cosa. Per què hem de recompilar moltes coses si canviem un atribut privat de la classe ?. L’ideal seria poder canviar les coses internes de la classe sense que calgui recompilar res més, al capdavall, l’atribut és privat i ningú l’utilitza directament.
És bastant habitual en programació orientada a objectes l’ús d’interfícies per fer que les classes no depenguin entre si. En el cas de C ++ l’ús d’interfícies és útil a més per evitar recompilados innecessaris.
Una interfície no és més que una classe en la qual es defineixen els mètodes públics necessaris, però no s’implementen. Després la classe concreta que vulguem fer hereta d’aquesta interfície i implementa els seus mètodes.
En el nostre cas, podem fer una classe InterfaceFecha, amb els mètodes públics virtuals purs (sense codi). Després la classe Data hereta de InterfaceFecha i implementa aquests mètodes.
Al fitxer InterfaceFecha.h tindríem
public: amor virtual void tomaFecha (int anho, intmes, int dia) = 0;
virtual int dameAnho () = 0;
virtual int dameMes () = 0;
virtual int dameDia () = 0;
virtual void metodoMaravilloso1 () = 0;
virtual void metodoMaravilloso2 () = 0; a};
De moment, ni tan sols existiria un InterfaceFecha.cc
La classe Data segueix igual, però hereta de InterfaceFecha.
class Data: public InterfaceFecha a {
public: a void tomaFecha (int anho, int mes, intdia); a int dameAnho (); a int dameMes (); a int dameDia (); a void metodoMaravilloso1 (); a void metodoMaravilloso2 ();
protected: amor int anho; // El anho amb quatre xifres, ex. 2004 a int mes; // El mes, d’1 a 12 de int dia; // El dia, d’1 a 31 de};
Ara, tot el que necessiti una data, ha de tenir un punter a InterfaceFecha en comptes de a Data. Algú instanciará Data i el guardarà en aquest punter. És a dir, podríem fer alguna cosa com això
InterfaceFecha * unaFecha = NULL;
…
unaFecha = new Data ();
unaFecha- > tomaFecha (2004, 1, 27);
…
delete unaFecha;
unaFecha = NULL;
Si ens fixem una mica, encara no hem arreglat res, excepte complicar l’assumpte. El que faci aquest codi necessita fer ara #include tant de InterfaceFecha.h com de Fecha.h. Si toquem alguna cosa en Fecha.h, aquest codi es recompilará.
Aquest codi necessita #include < Fecha.h > per poder fer el new de Data. Cal buscar la forma d’evitar aquest new. Sol ser també força habitual fer una classe (o utilitzar la mateixa Interface si el llenguatge ho permet, com és el cas de C ++) per posar un mètode estàtic que faci el new i ens el torni.
En el cas de Java, a l’posar aquest mètode, ja no tindríem una interfície, sinó una classe. Fer que Data hereti de InterfaceFecha ens limita a no heretar d’una altra cosa (Java no admet herència múltiple). Si això és admissible, podem fer-ho així. Si necessitem que Data hereti d’una altra classe, en comptes de posar el mètode estàtic a la interfície, hem de fer una tercera classe a part GeneradorFecha amb aquest mètode estàtic.
En el nostre exemple de C ++, la classe InterfaceFecha quedaria.
staticInterfaceFecha * dameNuevaFecha ();
virtual voidtomaFecha (intanho, intmes, int dia) = 0;
virtual int dameAnho () = 0;
virtual int dameMes () = 0;
virtual int dameDia () = 0;
virtual void metodoMaravilloso1 () = 0;
virtual void metodoMaravilloso2 () = 0; a};
Ara sí necessitem un InterfaceFecha.cc. Dins d’ell hauriem
InterfaceFecha * InterfaceFecha :: dameNuevaFecha () a {a return new Data (); a}
El codi que abans utilitzava el punter a InterfaceFecha quedaria ara
… a InterfaceFecha * unaFecha = NULL;
…
unaFecha = InterfaceFecha :: dameNuevaFecha ();
unaFecha- > tomaFecha (2004, 1, 27);
…
delete unaFecha;
unaFecha = NULL;
Com veiem, només cal el #include d’InterfaceFecha.h i aquest no inclou a Fecha.h (ho fa InterfaceFecha.cc, no el .h). Hem fet que aquest codi no vegi en absolut a Fecha.h. Ara podem tocar sense cap mirament el fitxer Fecha.h, que aquest codi no necessita ser reconstruït localment.
Un avantatge addicional és que es pot canviar la classe Data per una altra classe Fecha2 en temps d’execució. N’hi hauria prou amb posar un atribut estàtic en InterfaceFecha per indicar quina classe Data volem i fer que el mètode dameNuevaFecha () instancie i retorna una o una altra en funció d’aquest atribut.
Aquest mecanisme d’obtenir una instància d’una classe a través d’un mètode estàtic i d’una interfície, per no dependre de la classe concreta, crec que dins del món dels patrons de disseny és el patró Factoria.