c++ - ¿Cómo puedo eliminar/refactorizar una declaración de dependencia «amigo» correctamente?
refactoring friend (2)
El trasfondo de esta pregunta se basa en una muestra práctica en la que quería eliminar una dependencia de "amigo" de un par de clases que se utilizan para administrar el acceso bloqueado de lectura / escritura a un recurso compartido.
Aquí hay una abstracción del diseño estructural original para ese escenario:
Marcado en rojo, existe esta fea dependencia de "amigo" que quiero eliminar del diseño.
En resumen, ¿por qué tengo esto aquí:
-
ClassAProvider
comparte una referencia a unaClassA
través de varias instancias deClient
acceden simultáneamente -
Client
instancias delClient
deben acceder aClassA
únicamente a través de la clase auxiliarClassAAccessor
que gestiona lasClassAAccessor
internas -
ClassA
oculta todos los métodos destinados a ser utilizados deClassAAccessor
como protegidos. -
Por
ClassA
tanto,ClassA
puede garantizar que elClient
necesite usar una instancia deClassAAccessor
Este patrón es principalmente útil cuando se trata de garantizar que las instancias de la
ClassA
en un estado definido, si una operación del
Client
rescata (debido, por ejemplo, a una excepción no detectada).
Piense en la
ClassA
proporciona operaciones emparejadas (visibles internamente) como
lock()
/
unlock()
o
open()
/
close()
.
Las operaciones de reversión (estado) deberían llamarse en cualquier caso, especialmente cuando un cliente falla debido a una excepción.
Esto se puede manejar de manera segura a través del comportamiento del ciclo de vida del
ClassAAcessor
, la implementación del destructor puede garantizarlo.
El siguiente diagrama de secuencia ilustra cuál es el comportamiento previsto:
Además, las instancias de
Client
pueden lograr un control preciso para acceder a
ClassA
fácilmente, simplemente usando bloques de alcance C ++:
// ...
{
ClassAAccessor acc(provider.getClassA());
acc.lock();
// do something exception prone ...
} // safely unlock() ClassA
// ...
Todo bien hasta ahora, pero la dependencia de "amigo" entre
ClassA
y
ClassAAccessor
debería eliminarse por varias buenas razones
-
En la Superestructura UML 2.2, la Sección C.2 bajo Cambios de UML anterior dice:
The following table lists predefined standard elements for UML 1.x that are now obsolete. ... «friend» ...
The following table lists predefined standard elements for UML 1.x that are now obsolete. ... «friend» ...
- La mayoría de las reglas y pautas de codificación que he visto prohíben, o desaconsejan, usar un amigo, para evitar la dependencia de las clases de exportación a los amigos. Esto trae algunos problemas serios de mantenimiento.
Como dice el título de mi pregunta
¿Cómo puedo eliminar / refactorizar una declaración de amigo correctamente (preferiblemente comenzando en el diseño UML para mis clases)?
La dependencia no dice nada sobre el acceso de atributos u operaciones. ¡La dependencia se utiliza para representar la dependencia de definición entre los elementos del modelo! Qué hay de eliminar todas las dependencias de su modelo y aprender a usar la visibilidad. Si su relación de amigo representa el acceso a la característica (atributo u operación) desde un tipo específico (clase), puede establecer la visibilidad del atributo u operación en Paquete. La visibilidad del paquete significa que el valor del atributo es accesible desde instancias cuyas clases se definen en el mismo paquete.
Defina ClassAProvider y Client en el mismo paquete y establezca la visibilidad del atributo classA en Tipo de visibilidad del paquete. La instancia del cliente puede leer el valor de atributo de la clase A, pero las instancias de otros tipos no definidos en el mismo paquete no pueden.
Primero configuremos algunas restricciones para refactorizar:
- La interfaz públicamente visible de ClassAAccessor no debería cambiar de ninguna manera
- Las operaciones internas de Clase A no deben ser visibles / accesibles desde el público
- El rendimiento general y la huella del diseño original no deberían verse afectados
Paso 1: introduce una interfaz abstracta
Para una primera toma, factoricé el estereotipo «amigo» y lo reemplacé con una clase (interfaz)
InternalInterface
y las relaciones apropiadas.
Lo que constituía la dependencia de "amigo" se dividió en una relación de dependencia simple (azul) y una dependencia de "llamada" (verde) contra el nuevo elemento
InternalInterface
.
Paso 2: Mueva las operaciones que conforman la dependencia de "llamada" a la interfaz
El siguiente paso es madurar la dependencia de "llamada". Para hacer esto, cambio el diagrama de la siguiente manera:
-
La dependencia de «llamada» se convirtió en una asociación dirigida de
ClassAAccessor
aInternalInterface
(es decir,ClassAAccessor
contiene una variable privadainternalInterfaceRef
). -
Las operaciones en cuestión se trasladaron de
ClassA
aInternalInterface
. -
InternalInterface
se extiende con un constructor protegido, que es útil solo en herencia. -
ClassA
de «generalización» deClassA
aInternalInterface
está marcada comoprotected
, por lo que se hace públicamente invisible.
Paso 3: pegue todo en la implementación
En el paso final, necesitamos modelar una forma en que
ClassAAccessor
puede obtener una referencia a
InternalInterface
.
Como la generalización no es visible públicamente,
ClassAAcessor
ya no puede inicializarla desde la referencia de
ClassA
pasada en el constructor.
Pero
ClassA
puede acceder a
InternalInterface
y pasar una referencia usando un método adicional
setInternalInterfaceRef()
introducido en
ClassAAcessor
:
Aquí está la implementación de C ++:
class ClassAAccessor {
public:
ClassAAccessor(ClassA& classA);
void setInternalInterfaceRef(InternalInterface & newValue) {
internalInterfaceRef = &newValue;
}
private:
InternalInterface* internalInterfaceRef;
};
En realidad, se llama a este, cuando se llama al método recién introducido
ClassA::attachAccessor()
:
class ClassA : protected InternalInterface {
public:
// ...
attachAccessor(ClassAAccessor & accessor);
// ...
};
ClassA::attachAccessor(ClassAAccessor & accessor) {
accessor.setInternalInterfaceRef(*this); // The internal interface can be handed
// out here only, since it''s inherited
// in the protected scope.
}
Por lo tanto, el constructor de ClassAAccessor se puede reescribir de la siguiente manera:
ClassAAccessor::ClassAAccessor(ClassA& classA)
: internalInterfaceRef(0) {
classA.attachAccessor(*this);
}
Finalmente, puede desacoplar las implementaciones aún más, introduciendo otro
InternalClientInterface
como este:
Al menos es necesario mencionar que este enfoque tiene algunas desventajas en comparación con el uso de declaraciones de
friend
:
- Está complicando más el código
-
friend
no necesita introducir interfaces abstractas (que pueden afectar la huella, por lo que la restricción 3. no se cumple por completo) -
La relación de generalización
protected
no está bien soportada por la representación UML (tuve que usar esa restricción)