Diseodeinterfacesentrecomponentes
Diseo de interfaces entre componentes Estudio del grado y forma de acoplamiento entre partes de un sistema FernandoDodino,NicolsPasserini,FrancoBulgarelliVersin2.4Abril2015
1Interaccinentredoscomponentes1.1Digresin:porqucomponente?
2Interfacesentrantesysalientes2.1Interfazentrante2.2Interfazsaliente
3Ambientes3.1Todoenunsololugar3.2Distintosambientes
4Interfacessincrnicasyasincrnicas4.1Interfacessincrnicas4.2Interfacessincrnicas
4.2.1Buffers5AlgunaspalabrassobrelasFachadas(Faades)
5.1AdvertenciassobreelFaade5.2MalosusosdelFaade
6Ejercicioprctico:compracontarjetadecrdito6.1Interfazsaliente
6.1.1Cmoquedalacompraenelcdigo6.1.2ManejodeErroresenlainterfaz
6.2Interfazentrante6.2.1Cmodefinirlainterfazentrante
1de18
Diseodeinterfacesentrecomponentes
1 Interaccin entre dos componentes Volvemosunavezmsadecirqueunsistemaesunconjuntodecomponentesqueserelacionanparalograrunobjetivocomn.Ahorabien,cmoserelacionanesoscomponentes?
SupongamosqueA"necesitaalgo"deB.Loquenosvaainteresaresverqualternativastengoparamodelaresalneapunteada.Algunascosasatenerencuenta:
Direccinysentidodelacomunicacin: silacomunicacinvadeAhaciaB(controldirecto)oalrevs(inversinde
control) silacomunicacinentreAyBesbidireccionalounidireccional
SiAyBestnambosenelmismoambiente Manejodeerrores
qupasasihayunerrordeconexinconB 1 qupasasihayerrorcuandoBprocesaelpedidodeA
Gradodedependencia: siAesperaractivamenteaqueBterminedeprocesar(esdecir,sila
comunicacinessincrnica) opuedecontinuartrabajandosinbloquearse2(comunicacinasincrnica)
siAnecesitaalgnresultadodeBosisimplementedebenotificarloyolvidarse(fireandforget)
determinarsiinteresaqueBproceseelpedidodeAenformaexitosaparaqueAhagaotrascosasenlamismatransaccin
determinarsisenecesitareprocesar(enformamanualoautomtica)determinadosmensajesdeAaB
determinarsisenecesitaobtenerunlog(llevarunregistro)delospedidosenviadosaB
etcLasdecisionesquetomemosimpactarnenelgradoyformadeacoplamientoentreloscomponentesdenuestrosistema.Poreso,acontinuacintrataremosvariasdeestascuestiones.
1Leertambinapuntedemanejodeerrores2Leertambinapuntedecomunicacinentrecomponentes
2de18
Diseodeinterfacesentrecomponentes
1.1 Digresin: por qu componente? Lasideasqueestamosmencionandoaplicannoslocuandocomponente=objeto.Podemosmencionardiferentesnivelesparadisearcomponentes,enunaclasificacinquenoestaxativa,sinoqueintentaordenarunpoconuestrospensamientos:
Microdiseo,niveldeprograma,piensoenobjetos,clases,procedimientos,funciones
Nivelmedio,muchosobjetosformanuncomponente/mdulo/packageypiensolaintegracindeesascosas
Nivelarquitecturadesoftware,elnivelmsaltodentrodemiaplicacin,veoloselementosprincipalesdemiaplicacinycmoseintegranentres.
Nivel"arquitecturadesistema",pensandoelsistemacomounconjuntodepiezasdesoftwareointegracinentresistemas.Piensocmoseintegranlossistemas/aplicacionesentres.
Lospatronesqueveremosacontinuacinsonaplicablesacualquieradeestosniveles,msalldequeloscomponentesqueelijamosennuestrosejemplosseanpequeosporcuestionesdidcticas.
2 Interfaces entrantes y salientes Nosencontramosdiseandouncomponente.Primero,respondamosunapreguntafcilrespectoalproblemadelacomuncacin:estamosdiseandolaformadequeotroscomponentes"hablen"conelnuestro,olaformadeconectarnosaotrocomponente?Esdecir,estamosdiseandounainterfazentranteosaliente?Analicemosamboscasos.
Cuidado:esimportantenotarquelatecnologaenlaqueestnimplementadosloscomponentesquesecomunicarnconelnuestropodrasermuydiferenteaaquellaconlaquetrabajamos.
2.1 Interfaz entrante
Cuandoestamosenestasituacin,tenemosquepensarcualeslainformacinquenuestrocomponente(B,enelejemplo)necesitapararesolversutarea,ycmonosconviene
3de18
Diseodeinterfacesentrecomponentes
recibirla.Tendremosqueconsiderarquelasdecisionesquetomemosimpactarnencuanfcilesparaotrosmiembrosdeunequipodedesarrollousarestecomponente.
2.2 Interfaz saliente
Ahoranosencontramosenlasituacincontraria:estamosdesarrollandouncomponentequenecesitadelegarenotrociertastareas.Ahoratenemosquepreocuparnosporproveerlainformacinqueste(enelejemplo,B)necesita,adaptndonosalaformaquenosproponedecomunicacin.
3 Ambientes Debemosentenderqueeldiseodelacomunicacinentrecomponentesesbastantediferentecuandoloscomponentesestnenelmismoodiferenteambiente.
3.1 Todo en un solo lugar AyBpuedensermdulosqueconvivenenelmismoambiente.
Aqunotenemosmayoresrestriccionesencuantoalacomunicacin.Siestamostrabajandoconobjetosestoestansencillocomoenviarunmensajeaunobjetoyya.AyBsepiensancomocomponentesquetienenunaseparacinlgicaentres:
PorqueAoBpuedenestardesarrolladosporotros(objetosdelaJDK,deframeworksdeterceraspartes,degruposdetrabajoquenoformanpartedelamismagerenciaalaquepertenecemos,etc.)
AuncuandoAyBsoncomponentesquenosotrosmismosdesarrollamos,tratamosdereducirsuacoplamiento.Ojo,loscomponentestienenrelacinentresyestmuybienquelatengannoquieroeliminarlainteraccin,sevitarqueAconozca
4de18
Diseodeinterfacesentrecomponentes
demasiadodeBoviceversa.EntoncespuedopensarunainterfazquemepermitacambiarimplementacionesposiblesparaB:
porquenotengoimplementadoBperonecesitoirtesteandoA,entoncesarmounaimplementacinmnimadeBquerespondaalmensajedeAparaluegoreemplazarlaporlaimplementacinrealdeB
porqueBnotieneelalcancecerradoyquierodejarlapuertaabiertaparaloscambiosquesequevanavenir
oporcualquierotraraznquelojustifique
3.2 Distintos ambientes Enelsegundocaso,AyBestnimplementadosendistintoslugares(componenteremoto),distintastecnologasoinclusodesarrolladosbajodiferentesparadigmas.Sibienesimposibleindependizarsecompletamentedecuestionescomolatecnologaolaubicacindeestoscomponentes (estnlamismacomputadora?Odebemosaccederlos3atravsdelareddeInternet?),intentaremosqueestonosafectelomenosposible.Porqu?Porquenosinteresaminimizarelacoplamientoentreloscomponentes.As,siporejemploestamosdesarrollandocomponentesbajoelparadigmadeobjetos,nosinteresardisearlasinterfacesbajoesteparadigma.Siparapoderhacerqueloscomponentessecomuniquenconalgomuydiferente,sernecesariaunatransformacin,peronoserpartedemicomponente,sinoqueserunelementoindependientequesdependerdelatecnologa,peroquedeberaestardesacopladodelainterfaz.Entonceselobjetivoesseparar
lalgicadecomunicacinanivelnegocio:ququierodecir,quinformacindebopasar,quesperorecibir,querroresdenegociopodranocurrir
delalgicadetransformacinquefueranecesaria.
T es el componente que hace algn tipo de transformacin. Puede estar en alguno de los dos ambientes, o en otro ambiente aparte. No es objetivo de la materia desarrollar el componenteT.
3Losrequerimientosnofuncionales,aligualqueeldiablo,estnenlosdetalles
5de18
Diseodeinterfacesentrecomponentes
4 Interfaces sincrnicas y asincrnicas Otrapreguntaaresponderes:cuandoelcomponenteAsecomunicaconB,enqugradonecesitaAlosresultadosdeB?Tenemosdosopciones:comunicacionessincrnicasyasincrnicas.
4.1 Interfaces sincrnicas Sedacuandouncomponenteleenvaunmensajeaotro,stesequedaesperandoaqueelotrotermine.Estaeslaformaenquetrabajaelenvodemensajesenobjetos,laejecucindeprocedimientosenimperativo,olaaplicacindefuncionesenfuncional.Estambinlaformamssimpledetrabajar,porqueesfcilrazonarsobreella:siuncomponenteAleenvaunmensajeaotroB,yluegoAleenvootromensajeaC,sabemosconseguridadquelarespuestadeCllegardespusdelarespuestadeB.
Unejemplo:nosinteresaconocerlacotizacindeldlardelafechadehoy.SinosparamosdesdeelcomponenteAypensamosenhablarconuncomponenteBqueobtienelacotizacin,unaopcinsincrnicaeslasiguiente:BigDecimal dolarHoy = cotizador.obtenerCotizacion() Lointeresantedelsincronismoesquesabemosquesilalineaterminadeejecutarsesinerrores,tendremoscontotalseguridadunresultado,queusarloenelsiguienteenvodemensajes:BigDecimal precioFinal = calculadoraPrecios.obtenerPrecioFinal(dolarHoy)
LacontradeestaformadecomunicarnosesquesilacomunicacinentreAyBeslentaopococonfiable ,detendrlaejecucindelcomponenteAporquizsdemasiadotiempo.4
4.2 Interfaces asincrnicas CuandoAestencondicionesdecontinuarsutrabajomientrasBcalculaelresultado,oincluso,cuandoaAnoleimportaelresultadodeB,podemosimplementarinterfacesasincrnicas.Enestecaso,AleenviarunmensajeaBparasolicitareliniciodelatarea,conlosparmetrosquenecesite,yluegocontinuarsuflujodeejecucin.YentantoB,cuandotengaelresultadolatarealisto,dealgunaformacomunicaraAdeestesuceso.Algunasformasdehacerloson :5
depositarelresultadodelaoperacinenalgnlugardememoriacompartida,elcualAperidicamenterevisa
guardarunareferenciaaA,yenviarunmensajeastecuandohayaterminado
4Locualrequerirmuchasvecesreintentarlaoperacin5Verelapuntedepatronesdecomunicacinentrecomponentes
6de18
Diseodeinterfacesentrecomponentes
guardaruncallback
Ejemplo:podemosconstruirunainterfazasincrnicaparanuestrocotizadordelasiguienteforma:
1. EnunprimeromomentoelcomponenteAdisparaelmensajecotizador.obtenerCotizacion()
2. ...pasaeltiempo,yelcotizador(componenteB)ponelarespuestaenelcotizadorcotizador.setUltimaCotizacion(cotizacion)
3. Mstarde,elcomponenteAirabuscarlarespuestaacotizador.ultimaCotizacionOtambinpodramosusarcallbacks(bloquesdecdigo):cotizador.obtenerCotizacion(\resultado -> ..usar resultado...) YluegoBseencargardeejecutarelcallbackcuandoseanecesario.
Comocasoextremos,podradarsequeaAnoleimportaraqueBrealmentelerespondaalgo,sinotansoloqueseveanotificado.Enestecaso,AslosedebeenviarleunmensajeaBconlainformacinquenecesite,sinpreocuparseporcapturarunresultado.Unainterfazsincrnicaquenoexponeunresultado,esmsfcilmenteconvertibleaunainterfazasincrnicaqueunaquesexponeunresultado.
4.2.1 Buffers Muchasveces,parapoderlidiarconelasincronismoyelhechodequeAnoprocesarinstantneamenteelresultadodeB,oqueBnoiniciarinstantneamentelatareasolicitadaporA,esquenecesitaremosunbufferquealmacenetemporalmenteelresultadoy/oparmetrosdelatarea.SibienestebufferpuedeserunapartedelcomponenteBynomereceunestudioaparte,otrasvecesestebufferesexterno,ytantoAcomoBdebernempezarahablarconbuffer,enlugardeentreellos.
7de18
Diseodeinterfacesentrecomponentes
1. Aenvaelmensajealbuffer(enlugardeenvirseloaB)2. Unprocesotomalainformacindelbuffer,
a. envaunmensajeaBb. ylarespuestadeesemensajeseenvaaA.
Desde la perspectiva de A, necesitamos una interfaz entrante para recibir el resultado, con lo cualloqueanteserauncasodeusoahorasondos:
elpedidodeunaaccinaB el procesamiento de la respuesta de B ante ese pedido, donde se acepta o rechaza
latransaccin.DesdelaperspectivadeBtenemosvariasopciones:
si el buffer tiene cierta inteligencia, entonces B no se ve afectado por el mecanismo sincrnico / asincrnico de la interfaz: sigue siendo una interfaz entrante y no hay muchoscambiosenlaimplementacin.
si por el contrario B toma la responsabilidad de avisar el resultado a A, la interfaz entrante disparar a su vez una interfaz saliente con resultado ok o error una vez procesadoelpedido.Estoseveenlafigurasiguiente:
8de18
Diseodeinterfacesentrecomponentes
5 Algunas palabras sobre las Fachadas (Faades) Existe un patrn conocido como Faade cuya motivacin es proveer una abstraccin que permiteveraunconjuntodeobjetoscomosifueranunosolo.
Poresosesueleutilizarenalgunoscasosparamodelarinterfacesentrantes.Ejemplo: tenemos un sistema de pedidos. Cada pedido tiene lneas o tems (x cantidad de producto y), un criterio que define la urgencia y cuidado de los productos que conforman el pedidoyunestadoquedefinequhacerantedeterminadasacciones.
Paragenerarunpedido,
a. quieroagregar5kilosdequesoparmesano,2kilosdedulcedebatata
9de18
Diseodeinterfacesentrecomponentes
b. tambinquieroasignarleelestado(porejemplo,defaultenpendiente)c. ysetearleeltipodepedido.
SipensamosmtodosenPedidoparafacilitarestastareas:
a. paraagregaruntempublic void addItem(int cantidad, Producto producto) { this.items.add(new Item(cantidad, producto); }
b. paraasignarestadodefault:public Pedido() { ... this.estado = new Pendiente(); }
c. paraseteareltipodepedidopublic void setTipoPedidoMaximaSeguridad() { this.tipoPedido = new MaximaSeguridad(); } Entonces el Pedido est actuando como un Faade: el que genera un pedido nuevo no necesitaconocerlostems,nilassubclasesdeestadonilassubclasesdeltipodepedido.
10de18
Diseodeinterfacesentrecomponentes
El Faade me permite tener un punto de acceso desde el cual ofrecer servicios a componentes externos. Eso permite a los dems hablar con un nico componente nuestro y eliminalanecesidaddeconocernuestromodelo.
5.1 Advertencias sobre el Faade No obstante no es necesario tener siempre un objeto que oficie de fachada, en especial si lo nico que hace es redirigir el mensaje al objeto de negocio que hace el trabajo que corresponde.Ejemplo:enlainactivacinapareceunobjetoInactivacionFaade
QuhaceelmtodoinactivardeInactivacionFaade?public void inactivar(Cliente cliente) { cliente.inactivar(); }Noagreganingnvalor,solamenteforwardeaelpedidoalcliente.Adems, si el objetivo del Faade es ver un conjunto de objetos como si fueran uno solo eso noseestcumpliendo:estamosrecibiendounobjetoclientecomoparmetropublic void inactivar(Cliente cliente) { cliente.inactivar(); } Entonces si el que invoca a InactivacionFaade tiene que obtener de alguna manera al cliente,lapreguntaes:porqunoleenvaelmensajeinactivar()directamentealcliente?Corolario: solamente usar Faade cuando usamos un objeto que simplifica la interaccin con un mdulo de un sistema que puede ser bastante complejo (como en el caso de Pedido quepermitecentralizarlasaccionessobrelosdemsobjetosquemanejansuestado).Corolario 2: En el comn de los casos la interfaz entrante se modela con un mensaje a un objetodenegocioyconesonosalcanza.
5.2 Malos usos del Faade Suele ser un error comn del diseador novato confundir al Faade con un Manager, que quieresuplantaralobjetodedominiotomandodecisionesquenosonpropias:>>InactivarFaade
11de18
Diseodeinterfacesentrecomponentes
public void inactivar(Cliente cliente) { cliente.setEstado("I"); } Si el Faade toma decisiones que deberan estar en cliente (o en producto o en documento de compra), es seal de que estamos yendo hacia un mal diseo: el cliente se vuelve una estructuradedatospasivasobrelacualotrosobjetosmodificansuestadointerno.
La inactivacin en s misma no es un God Object, pero s la suma de los objetos estereotipados como . El resultado es un diseo pobre que no se parece en nada a las ideas de objetos: cualquier cambio en la estructura de Cliente repercute en todos losobjetosquehacenlasacciones.
6 Ejercicio prctico: compra con tarjeta de crdito AhoraveremoscmomodelarunainterfazatravsdeunejemploconcretoTenemosunsistemadeventasdeuncomerciocualquieraquetienecuentacorrienteconsusclientes.NecesitamosquecadaventaqueserealicecontarjetadecrditonotifiquealsistemaCreditSysteminformando:
Nmerodetarjetadecrdito Montodelaventa Descripcindelaoperacin Comercioorigen Fechayhora
Tenemosunbosquejodeldiagramadeclases:
12de18
Diseodeinterfacesentrecomponentes
No nos importa cmo se termina de resolver el mtodo comprar, slo nos interesa agregar una responsabilidad ms: disparar la interfaz saliente contra el sistema de Tarjeta de Crdito.
6.1 Interfaz saliente Doscosasalrespecto:
1. Estbuenollevaresaresponsabilidadaunobjetoaparte2. Por otra parte, aun si quisiera poner toda la responsabilidad en Cliente, cmo me
comunico con Credit System? No sabemos cmo implementar esa interfaz: el enunciadonodicenadaalrespecto.
Yenrealidadnodicenadaporquenoesrelevanteparaestamateria.Podemospensarmuchassolucionestcnicas:
RPC(RemoteProcedureCall) Socketpuro WebService
Cualquiera sea la tecnologa que elijamos, podemos abstraer la idea de notificar la compra deunclientequetienetarjetadecrdito.Sabemosqueunaabstraccinpuedeimplementarseenobjetosdemuchasmaneras:
Conunobjeto Conunaclase Conunainterfaz(ladeJava) Conunmtodo Conunmensaje
etc.En nuestro caso, podemos pensar en una interfaz de Java para modelar la notificacin de la compra (interfaz saliente entre componente A, nuestra aplicacin y componente B, una aplicacinespecficaparaTarjetasdeCrdito):
Este objeto S ser de una clase que implemente la interfaz que estamos definiendo ahora.
13de18
Diseodeinterfacesentrecomponentes
Lo importante es que yo no necesite cambiar la firma del mtodo cuando tenga que implementarlacomunicacinposta.
Algunasopcionesposiblesparadefinirlainterfaz:
a. notificarCompra(Integer numeroTarjeta, BigDecimal monto, String operacion, String CUITComercioOrigen, Date fechaCompra)
b. notificarCompra(Cliente cliente,Productoproducto,Stringoperacion,String CUITComercioOrigen, Date fechaCompra)
c. notificarCompra(Compra compra)
a) soporta menos cambios en la interfaz que b): si en algn momento nos piden que enviemos la razn social del cliente eso nos fuerza a cambiar la firma del mtodo de a).
Tanto a como b proponen un contrato con 5 parmetros!, proponer una abstraccin que relacione los elementos que intervienen en la compra es una tcnica apropiada para reducir el impacto de cualquier cambio futuro (la heurstica nos dice que hay altas chances de que un mtodo que recibe 5 parmetros sufra alguna modificacin en el futuro cercano). Esta abstraccin puede incluso ser til para el negocio, como enelcasodelaCompra.
Endefinitiva,loimportantees:
reconocerquenecesitamosponerlaresponsabilidadenunainterfazy saberquinformacinvaanecesitarelquetermineimplementandoesainterfaz
El contrato (la definicin del mensaje con los parmetros) es lo que se pide en la materia.
14de18
Diseodeinterfacesentrecomponentes
Yeldiagramadeclasesqueda:
TODO:Cambiarlafirmadelmtodo
sinnecesidaddedeterminarlaimplementacindedichainterfaz.
6.1.1 Cmo queda la compra en el cdigo Una opcin posible es acceder al sistema Credit System desde un nico punto de acceso global(Singleton).>>Cliente public void comprar(Producto producto) { ... if (this.tieneTarjetaDeCredito()) { TarjetaCreditoPosta.getInstance().notificarCompra(this, producto, "Compra xx", Comercio.CUIT_Origen, new Date()); } ... }
15de18
Diseodeinterfacesentrecomponentes
Otra opcin es tener conjunto de interesados (observers, listeners) que implementan la interfazsaliente:>>Clientepublicvoidcomprar(Productoproducto){
...for(ICompraObservercompraObserver:this.comprasObservers){
compraObserver.notificarCompra(this,producto,"Compraxx",Comercio.CUIT_Origen,newDate())
}...
}
Ambas opciones son vlidas, aunque es importante recalcar que si nuestra intencin es nicamente disparar una interfaz saliente y no nos interesa poder activar o desactivar esa interfaz, el Observer es un caso de sobrediseo: nuestra solucin provee una complejidad mucho mayor al problema que quiere resolver. Por otra parte s es correcto que pensemos en el Observer cuando en el evento que activa la notificacin a esa interfaz saliente hay otrosinteresadosoexisteesaposibilidad.
6.1.2 Manejo de Errores en la interfaz Qusucedesitenemosquemanejarerroresalllamaralainterfaz?Si seguimos pensando dentro del paradigma de objetos, los errores los manejamos con excepcionesynoconcdigosderetorno.Entonces preferimos pensar en notificarCompra como void, y no como un int notificarCompra.Querrorespodemosrecibir?
Errores en la conexin con el host remoto, error en la comunicacin (ruido o corte de conexin),erroresdepermisoenelacceso,erroresenelformatodelmensaje,etc.
Erroresdeprogramaalejecutarelprocesamientodelmensaje Errores de negocio: El cliente no existe, El monto no puede superar los $ 1.000 por
operacin,etc.Los dos primeros son errores de sistema/programa. En el ltimo caso tenemos errores de
16de18
Diseodeinterfacesentrecomponentes
negocio. Dependiendo de cada caso quizs tengamos que contemplar acciones posibles o simplementemostrarelmensajedeerrorenlapantalla(interfazdeusuario).Si la tecnologa que usamos para comunicar A con B no tiene mecanismo de excepciones y nos devuelve un cdigo de error, es en definitiva problema de quien implemente la interfaz ITarjetaCredito lo ideal es convertir ese cdigo de error 30018 en una excepcin que sea lanzada para quien pueda atraparla correctamente (en muy pocos casos un objeto de negocioyenlamayoradelasvecesunaventanadeusuario).Entonces la implementacin posta de la tarjeta de crdito acta como un objeto wrapper queenvuelvelallamadareal(queretornaelint)yloconvierteenunaexcepcin: >>Implementacin de la tarjeta de crdito public void notificarCompra(...) { ... intcodigoError=... ...; if (codigoError == 30018) { throw new SystemException("... ..."); } if (codigoError == 30015) { throw new BusinessException("El monto no puedesuperarlos$1.000por operacin"); } ... }
6.2 Interfaz entrante Enelmismosistemaqueremosmodelarlainactivacindeuncliente,quepuededispararse:
a. Desdelainterfazdeusuariodenuestraaplicacin
b. Desde el sistema de anlisis crediticio Customer Checker, un enlatado que no
formapartedenuestraaplicacinLosclientesinactivosnopuedenhacercompras.Estamosmodelandoahoraunainterfazentrantecombinandodosescenariosposibles:
17de18
Diseodeinterfacesentrecomponentes
Para el caso a): un mdulo que reside en el mismo ambiente del negocio (o no, dependiendodequtecnologaelijamosparadesarrollarlainterfazdeusuario)
Parab):uncomponenteexternoQuin es responsable de inactivar un cliente? El cliente, por supuesto, respondiendo al mensajeinactivar():
Alosfinesprcticosdeesteapuntenoimportamucho
cmosecodificaelmtodoinactivar() quvalidacinhayqueagregaralmtodocomprar()
sabemosquehabrunimpactoenesosmtodosynosalcanzaconeso.
6.2.1 Cmo definir la interfaz entrante Para modelar una interfaz entrante basta con definir qu objeto de negocio es el que responde a ese pedido: la tecnologa har la magia de convertir ese pedido en un mensaje paradichoobjeto.
Nro.cliente Fecha Activar/Inactivar
102875 10/11/2009 1
18de18
Top Related