c++ refactoring friend

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í:

  1. ClassAProvider comparte una referencia a una ClassA través de varias instancias de Client acceden simultáneamente
  2. Client instancias del Client deben acceder a ClassA únicamente a través de la clase auxiliar ClassAAccessor que gestiona las ClassAAccessor internas
  3. ClassA oculta todos los métodos destinados a ser utilizados de ClassAAccessor como protegidos.
  4. Por ClassA tanto, ClassA puede garantizar que el Client necesite usar una instancia de ClassAAccessor

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

  1. 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» ...
  2. 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:

  1. La interfaz públicamente visible de ClassAAccessor no debería cambiar de ninguna manera
  2. Las operaciones internas de Clase A no deben ser visibles / accesibles desde el público
  3. 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 a InternalInterface (es decir, ClassAAccessor contiene una variable privada internalInterfaceRef ).
  • Las operaciones en cuestión se trasladaron de ClassA a InternalInterface .
  • InternalInterface se extiende con un constructor protegido, que es útil solo en herencia.
  • ClassA de «generalización» de ClassA a InternalInterface está marcada como protected , 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 :

  1. Está complicando más el código
  2. friend no necesita introducir interfaces abstractas (que pueden afectar la huella, por lo que la restricción 3. no se cumple por completo)
  3. La relación de generalización protected no está bien soportada por la representación UML (tuve que usar esa restricción)