Hola,
Buenas a todos.
Tengo una preguntita filosa, y ya me corté demasiado tratando de responderme. A ver si algún instruído me da alguna línea porque estoy completamente OFF.
Advertencia: los temas discutidos a continuación pueden derivar en una enfermedad que puede resultar difícil de curar... patronitis. ¡Si... voy a hacer incapié a los no muy queridos patrones! por una buena cantidad de desarrolladores.
Les empiezo a relatar el panorama.
El macro objetivo que me planteo es disponer de una Fachada; pero no es una cualquiera. Por lo general las Fachadas se implementan para delegar sus funciones a un grupo de clases, y ofrecer una interfaz única o común de acceso a cierta parte. Vista desde afuera, la fachada actúa como si se comportase un subsistema y sirven para ocultar detalles que a la(s) clase(s) cliente(s) poco le deberían interesar.
Como fachada, por lo generalmente son únicas y se las estila implementar como Singleton... es aquí mi problema. Pero necesito ofrecer más detalles para que vean el porqué.
Mi Fachada tiene como naturaleza el comportamiento de un Sujeto/Observador; algo habitual y de esperarse de una fachada. Como Sujeto le permite enviar mensajes al exterior y comunicarse con clases "Oyentes" u "Observadores" de capas superiores. Como Observador puede enterarse de algunos comportamientos internos o de capas inferiores.
Debido a naturaleza de mi problema, me resulta conveniente disponer no de una única Fachada sino de múltiples que no es más que un principio del "Divide y vencerás" consiguiendo así lo que en la jerga se llaman Fachada de Sesión, o Fachadas de Caso de Uso. En mi caso el diseño está dirigido por Caso de Uso.
Pues eso... ya tengo mis Fachadas de Caso de Uso, que trabajan bien… interna y operativamente. Aparece ahora, como detalle menor una clase Coordinador que las controla. Esto dio origen a que aparezca una delgadita capa de Aplicación, y ahora estas Fachadas simplemente “ocultan” y delegan las verdaderas implementaciones a la capa Dominio, quien en realidad concentra el verdadero corazón de la lógica como bien sabemos.
Hasta allí se entiende, y todo está de maravillas.
Es aquí en donde puse mi fuerte a fin de ganar estabilización en el acceso al Dominio. Dentro del Dominio, existen también clases controladoras, que regulan y centralizan el acceso a clases que son aprovechadas desde diferentes lados. Es como una autopista en donde hay reciprocidades, vínculos que pueden ir y cambiar de lado.
Lo básico: Implementar Sujeto/Observador
Hay muchas maneras de llegar a este patrón, y cada una tiene sus pros y contras. Debido a que por característica de mi sistema los oyentes no tienen una interfaz común y cada uno tiene sus particularidades y en donde los casos de uso se pueden ir dando y ejecutando no necesariamente en un orden pre-establecido (bueno, inicialmente cuando no hay otra actividad y se está en su etapa más elemental si existe linealidad pero de allí en mas las cosas se van dando azarosamente) debía conseguir un diseño dinámico en donde cada fachada (Sujeto) pudiera interactuar con diferentes Observadores. Además estos "enlaces" deben ofrecer un buen equilibrio de acoplamiento y que no esté demasiado apegado a un grupo concreto y definido de clases (vamos… a una rama puntual dentro del árbol).
Como aparecen muchos Sujetos/Observadores (y no únicamente en esta capa, sino también en Dominio, y en una capa Base que tiene un mini y ultrabásico framework de persistencia) aprovecho la idea de definir un Sujeto/Observador abstracto que me aporte el cuerpo básico y de allí poder hacer herencias y donde cada “dupla” de Sujeto/Observador amplíe el concepto y defina sus propios eventos en base a la naturaleza de sus intereses. Además esto está motivado porque ya disponía de una capa de Servicio Técnico que me da apoyo a muchas cosas para el Dominio y superiores. ¡Que mejor que agregar también este Observer abstracto reutilizable! Y que me podría llevar a otros lados.
Así es que llego a tener una interfaz IAbstractObserver y IAbstractSubject. Además, de una clase TAbstractSubject que implementa esta última.
IAbstractObserver define los métodos para suscribirse/desuscribirse y TAbstractSubject los implementa y añade lo necesario para operar con los observadores. Para mantener la lista de observadores se apoya en la clase TInterfaceList.
Es así que llegué a este código:
unit UObserver; interface uses SysUtils, Classes; resourcestring rsc_out_of_range = 'Index out of range (%d)'; rsc_too_many_observers = 'Too many Observers suscribed'; type // Excepciones contempladas ETooManyObserversException = Exception; EOutOfRangeObserversException = Exception; IAbstractObserver = interface ['{A914C135-2D64-419C-BC4A-C501C73F4071}'] end; IAbstractSubject = interface ['{1ED689AE-E68F-4897-B065-9DE09321D078}'] function Suscribe(Observer: IAbstractObserver): integer; end; TAbstractSubject = class(TInterfacedObject, IAbstractSubject) private FObservers: TInterfaceList; function GetObserver(Index: Integer): IAbstractObserver; procedure SetObserver(Index: Integer; Observer: IAbstractObserver); public constructor Create; destructor Destroy; override; function Counts: Integer; property Observers[I: integer]: IAbstractObserver read GetObserver write SetObserver; end; implementation { TAbstractSubject } function TAbstractSubject.Counts: Integer; begin end; constructor TAbstractSubject.Create; begin inherited Create; FObservers := TInterfaceList.Create; end; destructor TAbstractSubject.Destroy; begin then raise ETooManyObserversException.Create(rsc_too_many_observers); FObservers.Free; inherited Destroy; end; function TAbstractSubject.GetObserver(Index: Integer): IAbstractObserver; begin then Result := FObservers[Index] as IAbstractObserver else raise EOutOfRangeObserversException.Create(Format(rsc_out_of_range,[Index])); end; procedure TAbstractSubject.SetObserver(Index: Integer; Observer: IAbstractObserver); begin then FObservers[Index] := Observer as IAbstractObserver else raise EOutOfRangeObserversException.Create(Format(rsc_out_of_range,[Index])); end; function TAbstractSubject.Suscribe(Observer: IAbstractObserver): integer; begin Result := FObservers.Add(Observer); end; function TAbstractSubject.UnSuscribe(Observer: IAbstractObserver): integer; begin Result := FObservers.Remove(Observer); end; procedure TAbstractSubject.UnSuscribe(Index: integer); begin then FObservers.Delete(Index) else EOutOfRangeObserversException.Create(Format(rsc_out_of_range,[Index])); end; end.
Aquí es donde se extiende el concepto y adquiere forma ya cada Sujeto, y en lo particular a la capa de aplicación en las Fachadas. La idea es ahora que para cada contexto de Sujeto/Objeto se herede una interfaz desde IAbstractObserver y una clase desde TAbstractSubject, para ejemplo digamos… IObserverReal y TSubjectReal… o si prefieren para ilustrarlo mejor: IConcretesubject e IConcreteObserver.
Ahora será responsabilidad de cada clase Sujeto/Observador definir su “contrato” y establecer que eventos van a notificar y dar respuestas. Por bajar a tierra, digamos que TSujetoVentas y IObservadorVentas trabajan con eventos que hacen a las ventas: EventoNuevaVenta, EventoVentaActualizada; mientras que TSujetoRecibo y IObservadorRecibo tienen lo suyo para los recibos, y así etc.
¿Se entiende?
Lo maravilloso de que el sujeto espere una interfaz es que se desapega de cualquier clase, justo lo que busco.
Una vez entendido en forma abstracta, vamos a lo concreto.
Gracias al lindo poder de los eventos, damos forma a los contratos. A modo de ejemplo digamos que trabajaremos con esto:
type // eventos que los observadores deben implementar TNewValueEvent = procedure(Sender: TObject; value: Integer) of object; TChangeValueEvent = procedure(Sender: TObject; value: string) of object;
Sender es el Sujeto que notifica, y value… la info que cambia o lo que interesa publicar. Es simple: quien y qué. Podría definirse contratos más complejos como:
TNewStateOfSalesEvent = procedure(Sender: TObject; Sale: TSale; var State: TSaleState) of object;
La idea es demostrarle el principio y no llenar de eventos más complicados.
Extiendo:
type IObserverReal = interface(IAbstractObserver) ['{2FA0A572-80FA-4090-83E5-3CE606EF307D}'] // evento: TNewValueEvent function GetNewValueEvent: TNewValueEvent; procedure SetNewValueEvent(Event: TNewValueEvent); // evento: TChangeValueEvent function GetChangeValueEvent: TChangeValueEvent; procedure SetChangeValueEvent(Event: TChangeValueEvent); // propiedades para los eventos property OnNewValueEvent: TNewValueEvent read GetNewValueEvent write SetNewValueEvent; property onChangeValueEvent: TChangeValueEvent read GetChangeValueEvent write SetChangeValueEvent; end;
TSubjectReal = class(TAbstractSubject) private FNewValue: integer; FChangeValue: string; procedure SetNewValue(Value: Integer); procedure SetChangeValue(Value: string); // se "sobreescriben" los métodos de la propiedad vectorial // para no tener que estar empleando constantemente moldeos function GetObserver(Index: Integer): IObserverReal; procedure SetObserver(Index: Integer; Observer: IObserverReal); public // métodos para notificación procedure NotifyNewValue; procedure NotifyChangeValue; // se "sobreescriben" (ocultan) los métodos Suscribe y UnSuscribe para que // cepten el tipo de observador concreto function Suscribe(Observer: IObserverReal): Integer; function UnSuscribe(Observer: IObserverReal): Integer; // los valores a los campos se hacen por propiedad property NewValue: Integer read FNewValue write SetNewValue; property ChangeValue: string read FChangeValue write SetChangeValue; // aqui la 2da parte del truco para sobreescribir la propiedad // vectorial por defecto property Observers[I: Integer]: IObserverReal read GetOBserver write SetObserver; default; end;
Creo que pueden darse una idea con sólo ver la definición de cómo se podrían implementar. Aquí la muestra para aclararles:
function TSubjectReal.GetObserver(Index: Integer): IObserverReal; begin Result := inherited Observers[Index] as IObserverReal; end; procedure TSubjectReal.NotifyChangeValue; var j, n: Integer; event: TChangeValueEvent; begin n := Counts; if n > 0 then begin for j := 0 to n - 1 do begin event := Observers[j].OnChangeValueEvent; if Assigned(event) then event(Self,FChangeValue); end; end; end; procedure TSubjectReal.NotifyNewValue; var j,n: Integer; event: TNewValueEvent; begin n := Counts; if n > 0 then begin for j := 0 to n - 1 do begin event := Observers[j].onNewValueEvent; if Assigned(event) then event(self,FNewValue); end; end; end; procedure TSubjectReal.SetChangeValue(Value: string); begin if Value <> FChangeValue then begin FChangeValue := Value; NotifyChangeValue; end; end; procedure TSubjectReal.SetNewValue(Value: Integer); begin if Value <> FNewValue then begin FNewValue := Value; NotifyNewValue; end; end; procedure TSubjectReal.SetObserver(Index: Integer; Observer: IObserverReal); begin inherited Observers[Index] := Observer; end; function TSubjectReal.Suscribe(Observer: IObserverReal): Integer; begin Result := inherited Suscribe(Observer); end; function TSubjectReal.UnSuscribe(Observer: IObserverReal): Integer; begin Result := inherited UnSuscribe(Observer); end;
Este esquema es moderadamente flexible, armonioso y extensible. Quizá tiene la desventaja de que el sujeto debe necesariamente heredar desde TInterfacedObject y existen escenarios en donde quizá no es deseable heredar de esta clase.
Después de todo hay otras maneras de llegar a patrón. Y seguramente se pueden aprovechar algunas ideas de esta singular propuesta y adaptarlas a otro esquema. Nomás presento un esquema que me parece interesante… y más en particular en lo que a hace a mis fachadas (que son mayoría; las otras clases Sujetos [que no fachadas] son una minoría y más que nada están pensadas para notificar a clases ajenas a sus “clases vecinas” sin generar más acoplamiento del necesario).
Ahora presentado lo que tengo, voy al problema. Como mencioné antes, las fachadas suelen ser únicas… y aquí no es la excepción. Quisiera que éstas sean y actúen como Singleton.
La idea más simple y evita problemas y no estar con demasiados patrones es directamente tener una variable a quien atacar desde las clases clientes e interesadas:
Initialization Instance := TSubjectReal.Create; Finalization Instance.Free;
Como con todo patrón… hay diversas maneras de llegar a Roma. Un método yendo a lo purista pasa por aprovechar los métodos NewInstance y FreeInstance y sobrescribirlos, y forzar la creación de una única instancia y devolver ésta.
Otros enfoques que pueden servir son sobrescribir y definir un propio constructor y destructor, también se puede dejar al público una función de tipo GetInstance() que encapsule el trabajo pesado… o una mezcla de todo esto.
El asunto es que me gustaría que los observadores, y las potenciales clases clientes, tengan visibilidad de atributo de este singleton (después de todo, ¡ya están acopladas!). Esto motivado principalmente para mantener una comunicación dual Sujeto < = > Observador más directa, y no tener que estar atacando a una función GetInstance() o acceder a la variable suelta Instance y que en realidad no hay control alguno de que en otro lado no haga un Create.
Habiendo comunicaciones desde y hacia varios lados donde intervienen varias fachadas y otros interesados en comunicarse, busco algo que me aporte más seguridad pero que no traiga consecuencias mayores a un rediseño en mis fachadas y sujetos singleton.
Aclaro que no todos los Sujetos son Singleton por lo que no me es viable la posibilidad de hacer Singleton a TAbstractSubject. De allí que en parte, cada Sujeto se defina o no como Singleton… La máxima posibilidad que se podría plantear es hacer una nueva herencia, desde TAbstractSubject, y tener un TAbstractSingletonSubject pero ahora resultará que sólo se permitirá la creación de una única instancia de entre alguna de los sujetos singleton concretos. Pero no quiero añadir más clases, quisiera evitar eso.
Intento analizarlo por el lado de sobrescribir NewInstance/FreeInstance que dentro de todo ofrece quizá la vía más segura de trabajar. Pero me marea y desconcierta el hecho de que no se trata únicamente de clases simples, sino que en fondo están las interfaces y esto toca de lleno el problema del conteo de referencia… en donde la liberación final tiene lugar cuando se llega a cuenta cero. Y sabiendo además que ya TIbterfacedObject redefine su NewInstance, y ahora que yo vuelva a tocarlo… ¡es para dudar!. He estado siguiendo las siguientes fuentes, tratando de encontrar la iluminación divina y de comprender en cómo llegar a un Singleton cuando se está operando con interfaces:
http://edn.embarcade...m/article/22576
www.delphi3000.com/article.asp?ID=1736
http://en.wikipedia....ngleton_pattern
http://es.wikipedia.org/wiki/Singleton
http://www.castle-ca...o.uk/single.htm
http://sourcemaking....terns/singleton
También se ha discutido esto en CD, y yo incluso participé:
http://www.clubdelph...ead.php?t=62510
Pero aún así, y sabiendo que esas puestas están basadas más que nada en objetos “puros” no me convence la forma de llevar a la práctica cuando se trata de interfaces.
No logro unir los conceptos entre los artículos y esto me confunde.
Habiendo presentado el contexto, y en base al código expuesto y el manejo de estas interfaces locas ¿Qué me aconsejan?
Disculpen semejante rollo, pero debía exponer lo que hice, y porqué para que me entiendan lo mejor posible.
Espero no aburrirlos.
Saludos,