Как правильно удалить / реорганизовать объявление "друга" зависимости?
История вопроса основана на практическом примере, в котором я хотел удалить зависимость "друг" из пары классов, которые используются для управления заблокированным доступом на чтение / запись к общему ресурсу.
Вот абстракция оригинального структурного проекта для этого сценария:
Выделенная красным, есть эта уродливая "дружеская" зависимость, которую я хочу удалить из дизайна.
Короче, зачем мне эта штука там:
ClassAProvider
делится ссылкой наClassA
в течение нескольких одновременных обращенийClient
экземплярыClient
экземпляры должны получить доступClassA
исключительно черезClassAAccessor
вспомогательный класс, который управляет внутреннимиClassA
скрывает все методы, предназначенные дляClassAAccessor
как защищено.- Так
ClassA
может гарантировать, чтоClient
необходимо использоватьClassAAccessor
пример
Этот шаблон в первую очередь полезен, когда речь идет о том, чтобы оставить экземпляры ClassA
в определенном состоянии, если Client
операция выручает (например, из-за необработанного исключения). Думать о ClassA
предоставление (внутренне видимых) парных операций, таких как lock()
/ unlock()
или же open()
/ close()
,
Операции реверсирования (состояния) должны вызываться в любом случае, особенно когда клиент падает из-за исключения.
Это может быть безопасно обработано через ClassAAcessor
В поведении жизненного цикла, деструктор реализации может обеспечить это. Следующая диаграмма последовательности иллюстрирует предполагаемое поведение:
Дополнительно Client
экземпляры могут обеспечить точный контроль доступа ClassA
легко, просто используя блоки контекста C++:
// ...
{
ClassAAccessor acc(provider.getClassA());
acc.lock();
// do something exception prone ...
} // safely unlock() ClassA
// ...
Пока все хорошо, но "дружеская" зависимость между ClassA
а также ClassAAccessor
должны быть удалены по ряду веских причин
- В надстройке UML 2.2, раздел C.2 в разделе "Изменения по сравнению с предыдущим UML" говорится:
The following table lists predefined standard elements for UML 1.x that are now obsolete. ... «friend» ...
- Большинство правил и руководств по кодированию, которые я видел, запрещают или настоятельно не рекомендуют использовать друга, чтобы избежать жесткой зависимости от экспортирующих классов для друзей. Эта вещь вызывает некоторые серьезные проблемы с обслуживанием.
Как говорится в заголовке моего вопроса
Как правильно удалить / реорганизовать объявление друга (желательно начиная с дизайна UML для моих классов)?
2 ответа
Давайте сначала установим некоторые ограничения для рефакторинга:
- Публично видимый интерфейс ClassAAccessor не должен меняться никоим образом
- Внутренние операции ClassA не должны быть видны / доступны для общественности
- Общая производительность и след оригинального дизайна не должны быть повреждены
Шаг 1: Введите абстрактный интерфейс
Для первого выстрела я вычеркнул стереотип "друга" и заменил его классом (интерфейсом)InternalInterface
и соответствующие отношения.
То, что составляло зависимость "друг", было разделено на простое отношение зависимости (синее) и зависимость "вызов" (зеленое) против нового InternalInterface
элемент.
Шаг 2: Переместите операции, составляющие зависимость "call", в интерфейс
Следующим шагом является формирование зависимости "call". Для этого я меняю диаграмму следующим образом:
- Зависимость "call" превратилась в направленную ассоциацию из
ClassAAccessor
кInternalInterface
(Т.е.ClassAAccessor
содержит приватную переменнуюinternalInterfaceRef
). - Указанные операции были перенесены из
ClassA
вInternalInterface
, InternalInterface
расширен с помощью защищенного конструктора, что полезно только в наследовании.ClassA
"обобщающее" объединениеInternalInterface
помечен какprotected
, так что это стало публично невидимым.
Шаг 3: склеить все вместе в реализации
На последнем этапе нам нужно смоделировать способ ClassAAccessor
можно получить ссылку на InternalInterface
, Поскольку обобщение не видно публично, ClassAAcessor
не может инициализировать его из ClassA
ссылка передается в конструктор больше. Но ClassA
может получить доступ InternalInterface
и передать ссылку, используя дополнительный метод setInternalInterfaceRef()
введено в ClassAAcessor
:
Вот реализация C++:
class ClassAAccessor {
public:
ClassAAccessor(ClassA& classA);
void setInternalInterfaceRef(InternalInterface & newValue) {
internalInterfaceRef = &newValue;
}
private:
InternalInterface* internalInterfaceRef;
};
Этот на самом деле называется, когда также недавно введенный метод 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.
}
Таким образом, конструктор ClassAAccessor может быть переписан следующим образом:
ClassAAccessor::ClassAAccessor(ClassA& classA)
: internalInterfaceRef(0) {
classA.attachAccessor(*this);
}
Наконец, вы можете отделить реализации еще больше, введя другой InternalClientInterface
как это:
По крайней мере, необходимо отметить, что этот подход имеет некоторые недостатки по сравнению с использованием friend
декларации:
- Это усложняет код больше
friend
не нужно вводить абстрактные интерфейсы (это может повлиять на площадь, поэтому ограничение 3. не полностью выполнено)protected
отношение обобщений не очень хорошо поддерживается представлением UML (мне пришлось использовать это ограничение)
Зависимость ничего не говорит о доступе к атрибутам или операциям. Зависимость используется для представления зависимости определения между элементами модели! Как насчет того, чтобы удалить все зависимости из вашей модели и узнать, как использовать видимость. Если ваши отношения дружбы представляют собой доступ к функции (атрибуту или операции) из определенного типа (класса), вы можете установить видимость атрибута или операции в Package. Видимость пакета означает, что значение атрибута доступно из экземпляров, классы которых определены в одном и том же пакете.
Определите ClassAProvider и Client в одном пакете и установите видимость атрибута classA в Тип видимости пакета. Экземпляр клиента может прочитать значение атрибута classA, но экземпляры других типов, не определенные в том же пакете, не могут.