Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

POO

Els conceptes i patrons d’aquest document són aplicables a qualsevol llenguatge orientat a objectes. S’inclouen notes específiques per a Java i C# perquè són dos dels llenguatges OOP amb tipatge estàtic més utilitzats, i tot i compartir els mateixos fonaments, difereixen en aspectes sintàctics i idiomàtics que val la pena conèixer.

Conceptes de POO

Abstracció

L’abstracció amaga la complexitat a l’usuari i només mostra la informació rellevant. Permet centrar-nos en el “què” en lloc del “com”. Els detalls d’un mètode abstracte són implementats de forma separada per cada classe.

Objectius: reusabilitat de codi, flexibilitat d’implementació, herència múltiple.

Java: classes abstractes (abstracció parcial) i interfícies (abstracció total). Sintaxi: abstract (classes abstractes) i interface, implements (interfícies).

C#: classes abstractes i interfícies, igual que Java. Sintaxi: abstract (classes abstractes) i interface, : (interfícies). Tant Java (des de Java 8) com C# (des de C# 8) permeten implementació per defecte en interfícies (default interface methods).

Encapsulació

L’encapsulació lliga les dades i els mètodes relacionats dins d’una classe. També protegeix les dades (estat) fent els camps privats i donant accés a ells només mitjançant els mètodes relacionats que implementen les regles de negoci i invariants.

Objectius: protecció de dades (estat), llegibilitat del codi.

Java: camps privats i mètodes públics (només els exposats). Sintaxi: private, setX(), getX().

C#: camps privats i propietats públiques amb accessors. Sintaxi: private, public Type Name { get; set; }. C# té suport natiu de propietats, evitant la necessitat de mètodes getters/setters explícits.

Herència

Amb l’objectiu de reusar codi, l’herència permet que una classe fill hereti les característiques (camps i mètodes) d’una altra classe pare. La relació es diu is-a. L’herència s’utilitza quan sabem que:

  1. Les dues classes es troben en el mateix domini lògic.
  2. La superclasse és un contracte que no canviarà (interfície o classe abstracta).
  3. Les millores realitzades per la subclasse són principalment additives.

L’alternativa és la composició.

Objectius: reusabilitat de codi, llegibilitat del codi.

Java: Classe pare i class fill. Alternativa vàlida: patrons de composició i delegació. Sintaxi: extends.

C#: mateixa mecànica que Java (herència simple de classes). Sintaxi: : (tant per herència com per implementació d’interfícies). C# també ofereix la paraula clau sealed (equivalent a final en Java) per evitar l’herència.

Composició

La composició és una alternativa de l’herència per a reusar codi. La relació es diu has-a. Es pot aconseguir utilitzant instàncies d’altres objectes. És una tècnica preferible a la herència, ja que redueix l’acoblament del codi.

Un tipus específic de composició és l’agregació, que no implica la propietat dels objectes interns i, per tant, tampoc la destrucció d’aquests quan l’objecte contenidor es destrueix.

Objectius: reusabilitat de codi.

Java: variables d’instància d’altres classes. Sintaxi: private ClassName instanceName.

C#: idèntic a Java. Sintaxi: private ClassName instanceName;. Es pot combinar amb propietats per exposar funcionalitat: public ClassName Name { get; }.

Polimorfisme

El polimorfisme permet que un mateix missatge tingui comportaments diferents segons l’objecte que el rep. Existeixen dos tipus: el polimorfisme estàtic (sobrecàrrega de mètodes, resolt en temps de compilació) i el polimorfisme dinàmic (sobreescriptura de mètodes, resolt en temps d’execució mitjançant dynamic binding), que permet substituir i estendre la funcionalitat d’objectes que comparteixen una mateixa interfície.

Objectius: llegibilitat del codi, flexibilitat del codi.

Java: sobreescriptura de mètode (dinàmic), menys rellevant: sobrecàrrega de mètode (estàtic). Sintaxi: myMethod(), myMethod(int x), myMethod(int x, String y) (estàtic) i ParentClass.myMethod(), ChildClass.myMethod() (dinàmic).

C#: mateixa mecànica. Diferència clau: en C# cal marcar explícitament els mètodes com a virtual a la classe pare i override a la classe fill per habilitar el polimorfisme dinàmic (en Java tots els mètodes no estàtics són virtuals per defecte). Sintaxi: virtual, override, new (per amagar mètodes).

Relacions entre objectes

A més dels conceptes anteriors (herència, composició i agregació), existeixen altres tipus de relacions entre objectes:

  • Associació: un objecte fa referència a un altre.
  • Dependència: un objecte rep una referència en una de les seves operacions.

El diagrama següent mostra totes les relacions, incloent les ja explicades. S’utilitzen com a exemple classes de la biblioteca estàndard de Java (Number, Double, Comparable), que tenen equivalents directes en C# (Object, Double, IComparable).

  1. Herència de Number i implementació de Comparable
  2. Associació entre Child1 i Parent1
  3. Associació navegable des de Child2 cap a Parent2
  4. Agregació: Wheel pot existir sense Car
  5. Composició: Heart no pot existir sense Human
  6. Dependència: Object1 usa l’Object2

Tipus d’objectes

Un cop entesos els conceptes i les relacions, podem classificar els objectes segons les seves característiques. Un objecte és una entitat que pot integrar comportament i estat. La pràctica més acceptada és la d’encapsular l’estat darrere del comportament, perquè formen part de la implementació, que és convenient amagar.

Una primera classificació dels objectes es pot fer en funció de dues característiques: si tenen estat (stateful) o no (stateless), i en el cas que en tinguin, si són immutables o no (mutables):

  • Un objecte té estat si el seu comportament depèn d’interaccions prèvies. Els objectes sense estat tenen un comportament esperable que només depèn dels paràmetres de les crides, com passa a la programació funcional.
  • Un objecte immutable té un estat que no pot canviar-se després de ser creat. Permeten escriure codi més fàcil d’entendre i segurs quan hi ha concurrència.

Segons el seu ús de les dades, podem fer una classificació dels tipus d’objectes que podem trobar més freqüentment en llenguatges orientats a objectes com Java i C#:

  • Abstract Data Types (ADT): són objectes amb comportament i estat encapsulat, i habitualment mutables. Cal definir detalladament el seu comportament, i evitar retornar referències a objectes mutables del seu estat.
  • Estructures de dades: són objectes amb només estat, i poden ser immutables. Permeten transferir dades o representar un estat. En Java s’utilitzen record (Java 16+); en C# s’utilitzen record (C# 9+) o struct.
  • Serveis: són objectes amb només comportament que poden operar sobre dades. També es poden anomenar utilitats o helpers.

Aquestes són propietats a l’hora de classificar classes d’objectes:

PropietatValorsExplicació
StateStateless / StatefulSi té estat o no
MutabilityMutable / ImmutableSi és modificable un cop creat o no
LifecycleOne-shot / ReusableSi es pot reutilitzar un cop feta la seva funció
ConcurrencyThread-safe / NotSi es pot compartir entre fils d’execució
TimingSynchronous / AsynchronousSi s’executa immediatament o ho fa després concurrentment
PurityPure / ImpureSi no té efectes secundaris i retorna sempre el mateix resultat per als mateixos paràmetres
OwnershipOwns Resources / NotSi és responsable de netejar recursos quan acaba
ExtensibilityOpen / ClosedSi el disseny permet o no estendre la classe

Patrons de disseny

Aplicant els conceptes, relacions i classificacions anteriors, podem identificar solucions recurrents als problemes de disseny. Un patró de disseny és una solució general a un problema comú i recurrent en el disseny de programari. Molts patrons implementen els principis de disseny com ara SOLID. Un patró de disseny no és un disseny acabat que es pot transformar directament en codi; és una descripció o plantilla per resoldre un problema que es pot utilitzar en moltes situacions diferents.

Tenim algunes categories generals:

  • De comportament: identifiquen patrons de comunicació entre objectes.
  • Estructurals: faciliten el disseny quan s’han d’establir relacions entre entitats.
  • Creacionals: relacionats amb mecanismes de creació d’objectes de la forma més adient per cada cas.
  • Concurrència: tracten el paradigma de programació multifil.

A continuació es mostren alguns patrons importants.

Patrons de comportament

Command

El patró command encapsula una sol·licitud com a objecte, de manera que us permetrà parametrizar altres objectes amb diferents peticions, cues o peticions de registre i donar suport a operacions reversibles.

Tenim dos actors principals: el Client crea el ConcreteCommand i assigna el seu Receiver. L’Invoker té Commands que pot executar.

En Java i C# la implementació és idèntica. En C#, el patró es pot simplificar utilitzant Action o Func<T> com a commands lleugers en lloc de crear classes dedicades.

Iterator

El patró iterator proporciona una manera d’accedir als elements d’un objecte agregat seqüencialment sense exposar la seva representació subjacent.

Exemples Java: java.util.Iterator i java.util.Enumeration

Exemples C#: IEnumerator<T> i IEnumerable<T>. C# també ofereix yield return per crear iteradors de forma simplificada.

Observer

El patró observer defineix una dependència entre molts objectes de manera que quan un objecte canvia d’estat, tots els seus dependents són notificats i actualitzats automàticament.

Exemples Java: java.util.EventListener

Exemples C#: C# implementa el patró observer de forma nativa amb event i delegate, fent innecessària la implementació manual del patró en la majoria de casos.

State

El patró state permet a un objecte alterar el seu comportament quan canvia el seu estat intern. L’objecte semblarà canviar de classe.

El Context pot tenir un nombre de States. Quan cridem request(), el que fem es cridar el handle corresponent del State actual. Un Client no coneix el funcionament intern dels States.

La implementació és equivalent en Java i C#. En C#, es pot combinar amb pattern matching (switch amb patrons de tipus) per simplificar la lògica de transició d’estats.

Strategy

El patró strategy s’utilitza quan tenim diversos algorismes per a una tasca específica i el client decideix que s’utilitzi la implementació real en temps d’execució.

És molt similar a State, però és el Client qui habitualment escull la Strategy.

Exemples Java: Collections.sort() amb el paràmetre Comparator.

Exemples C#: List<T>.Sort() amb el paràmetre IComparer<T>, o bé utilitzant lambdes Func<T, TResult> directament com a estratègies.

Dependency Injection

El patró de Dependency Injection permet a un objecte rebre les instàncies d’altres objectes de què depèn. Això permet que el client no s’hagi de referir a instàncies concretes, seguint el principi d’inversió de dependència. S’utilitza per implementar el principi de la inversió de control (IoC) mitjançant un contenidor.

Hi ha tres tipus d’injecció: Constructor, Setter i Interface.

  • Constructor: li diem al contenidor quina implementació s’utilitza per als paràmetres del constructor, que poden ser interfícies.
  • Setter: li diem al contenidor quines propietats (amb setters) cal instanciar d’un cert objecte.
  • Interface: li diem al contenidor que el nostre client rebrà les dependències mitjançant un mètode (del tipus setService).

Per utilitzar-los, cal configurar el contenidor. Es pot fer programàticament o mitjançant un arxiu de configuració.

L’objecte que injecta les dependències es diu injector, i els objectes de què depèn solen ser serveis.

En Java, els contenidors més comuns són Spring i CDI. En C#, el framework .NET inclou un contenidor de DI integrat (Microsoft.Extensions.DependencyInjection) amb suport natiu a ASP.NET Core.

Service Locator

El Service Locator és una alternativa al Dependency Injector, amb la particularitat que el codi client té una dependència del contenidor, que utilitza per localitzar tots els serveis que necessita. És a dir, no es produeix la injecció automàtica.

No es tracta d’una Factory, ja que no crea els serveis cada cop, només si cal. Es tracta més bé d’un registre.

Template method

El patró template method defineix l’esquelet d’un algorisme d’un mètode, diferint alguns passos a subclasses. Aquest patró permet que les subclasses redefineixin certs passos d’un algorisme sense canviar l’estructura de l’algorisme.

El templateMethod fa ús del subMethod. La implementació és idèntica en Java i C#: una classe abstracta amb un mètode virtual o abstract que les subclasses sobreescriuen.

Exemple: construcció d’una casa de fusta o de vidre

Patrons estructurals

Els patrons estructurals següents s’implementen de forma pràcticament idèntica en Java i C#, ja que depenen de conceptes comuns (interfícies, composició, herència).

Adapter

El patró adapter converteix la interfície d’una classe en una altra interfície que els clients esperen. L’adaptador permet que les classes treballin conjuntament, tot i tenir interfícies incompatibles.

El ConcreteAdapter està composat amb l’Adaptee, que li permet una operació addicional: adaptedOperation. Exemple: un capità que només pot utilitzar barques de rem i no pot navegar

Composite

El patró composite permet compondre objectes en estructures d’arbre per representar jerarquies parcials. Composite permet als clients tractar objectes individuals i composicions d’objectes de manera uniforme.

Tant els objectes individuals com els composats poden ser tractats igual amb operation.

Decorator

El patró decorator atribueix dinàmicament responsabilitats addicionals a un objecte. Els decorators proporcionen una alternativa flexible a la subclasse per ampliar la funcionalitat.

Podem afegir un comportament al ConcreteDecorator. Exemple: trol decorat amb una porra

Facade

El patró facade proporciona una interfície unificada i més senzilla a un conjunt d’interfícies d’un subsistema. La façana defineix una interfície de més alt nivell que facilita la utilització del subsistema, seguint el principi del “Least knowledge”.

Exemple: treballadors de la mina d’or

Proxy

El patró de proxy proporciona un substitut per a un altre objecte per controlar-ne l’accés.

Qualsevol Client pot tractar el Proxy com el RealSubject, ja que implementen Subject. Exemple: els tres mags que entren a la torre

Patrons creacionals

Els patrons creacionals estan relacionats amb mecanismes de creació d’objectes de la forma més adient per a cada cas, abstraient el procés d’instanciació. La implementació d’aquests patrons és equivalent en Java i C#.

Builder

El patró builder separa la construcció d’un objecte complex de la seva representació, de forma que el mateix procés de construcció pot generar diferents representacions.

Factory method

Factory method defineix una interfície per crear un objecte, però permet que les subclasses decideixin quina classe s’inicia. Permet diferir la instància de classe a subclasses.

El mètode factoryMethod és abstracte, i s’encarrega de crear Products. Permet seguir el principi de “Dependency inversion”: evitar dependències de tipus concrets. Exemple: el regne que necessita tres objectes temàtics

Singleton

El patró de Singleton assegura que una classe només té una instància i li proporciona un punt d’accés global.

Exemple: només pot haver una torre d’ivori

Referències

Last change: , commit: 405871d