Проблема OOP и макросов

Я столкнулся с этой проблемой через коллегу сегодня. У него была конструкция для передней системы, которая выглядит следующим образом:

class LWindow
{
   //Interface for common methods to Windows
};

class LListBox : public LWindow
{
   //Do not override methods in LWindow.
   //Interface for List specific stuff
}

class LComboBox : public LWindow{} //So on

Система Window должна работать на нескольких платформах. Предположим, что на данный момент мы нацелены на Windows и Linux. Для Windows у нас есть реализация для интерфейса в LWindow. И у нас есть несколько реализаций для всех LListBox es, LComboBox es и т.д. Моя реакция состояла в том, чтобы передать LWindow* (объект реализации) в базовый класс LWindow, чтобы он мог это сделать:

void LWindow::Move(int x, int y)
{
   p_Impl->Move(x, y);   //Impl is an LWindow*
}

И, сделайте то же самое для реализации LListBox и т.д.

Первоначально предложенное решение сильно отличалось. Это сводилось к следующему:

#define WindowsCommonImpl {//Set of overrides for LWindow methods}

class WinListBox : public LListBox
{
    WindowsCommonImpl     //The overrides for methods in LWindow will get pasted here.
    //LListBox overrides
}

//So on

Теперь, прочитав все о макросах, являющихся злыми и хорошими проектами, я сразу же был против этой схемы. В конце концов, это дублирование кода. Но я не мог убедить своего коллегу в этом. И я был удивлен, что это так. Итак, я задаю вам этот вопрос. Каковы возможные проблемы последнего метода? Я бы хотел получить практические ответы. Мне нужно убедить кого-то, кто очень практичен (и привык делать такие вещи. Он упомянул, что в MFC много макросов!), Что это плохо (и я сам). Не научить его эстетике. Кроме того, что-то не так с тем, что я предложил? Если да, то как мне его улучшить? Спасибо.

РЕДАКТИРОВАТЬ: Пожалуйста, дайте мне несколько причин, поэтому я могу чувствовать себя хорошо о себе, поддерживая oop: (

Идти за щедростью. Пожалуйста, спросите, нужны ли вам какие-либо разъяснения. Я хочу знать аргументы для и против OOP против макроса:)

Ответ 1

Ваш коллега, вероятно, думает о макросах MFC-сообщений; они используются в важных местах в каждом производном классе MFC, поэтому я могу видеть, откуда приходит ваш коллега. Однако это не для реализации интерфейсов, а скорее для деталей, взаимодействующих с остальной ОС Windows.

В частности, эти макросы реализуют часть системы насосов сообщений Windows, где "сообщения", представляющие запросы на классы MFC для работы, направляются на правильные функции обработчика (например, сопоставление сообщений с обработчиками). Если у вас есть доступ к визуальной студии, вы увидите, что эти макросы завершают записи в карте сообщений в несколько сложном массиве структур (который код вызывающей ОС знает, как читать) и предоставляют функции для доступа к этой карте.

Как пользователи MFC, макросистема делает этот вид чистым для нас. Но это работает главным образом потому, что базовый Windows API хорошо определен и не будет сильно изменяться, и большая часть макрокода создается с помощью IDE, чтобы избежать опечаток. Если вам нужно реализовать что-то, что связано с беспорядочными объявлениями, тогда макросы могут иметь смысл, но пока это, похоже, не так.

Практические проблемы, которые может заинтересовать ваш коллега:

  • дублированные макросы. Похоже, вам понадобится скопировать строку "WindowsCommonImpl" в каждое объявление класса - если макрос расширяется до некоторых встроенных функций. Если они только декларации, а реализации идут в отдельный макрос, вам также нужно будет сделать это в каждом файле .cpp - и каждый раз меняйте имя класса, передаваемое в макрос.
  • больше времени перекомпиляции. Для вашего решения, если вы что-то измените в реализации LWindow, вам, вероятно, потребуется перекомпилировать LWindow.cpp. Если вы что-то измените в макросе, все, что включает файл заголовка макроса, необходимо перекомпилировать, что, вероятно, является вашим целым проектом.
  • сложнее отладить. Если ошибка связана с логикой макроса, отладчик, вероятно, перейдет к вызывающему абоненту, где вы не увидите ошибку сразу. Возможно, вы даже не подумали проверить определение макроса, потому что считали, что знаете точно, что он сделал.

Таким образом, в основном ваше решение LWindow является лучшим решением для минимизации головных болей в будущем.

Ответ 2

Не может напрямую ответить на ваш вопрос, но не могу не сказать вам прочитать образец шаблона Bridge в GOF, Это означало именно для этого.

Отменить абстрагирование от чтобы эти два независимо друг от друга.

Из того, что я могу понять, вы уже на правильном пути, кроме материала MACRO.

Моя реакция состояла в том, чтобы пройти LWindow * (объект реализации) для базовый класс LWindow, чтобы он мог это сделать:

Ответ 3

LListBox и LComboBox должны получить экземпляр WindowsCommonImpl.

В первом решении наследование используется так, чтобы LListBox и LComboBox могли использовать некоторые распространенные методы. Однако наследование не предназначено для этого.

Ответ 4

Я согласен с тобой. Решение с макросом WindowsCommonImpl действительно плохое. Он подвержен ошибкам, трудно распространяться и очень трудно отлаживать. MFC - хороший пример того, как вы должны не создавать свою библиотеку окон. Если это похоже на MFC, вы действительно ошибаетесь.

Итак, ваше решение явно лучше, чем на основе макросов. Во всяком случае, я бы не согласился, что это достаточно хорошо. Самым существенным недостатком для меня является то, что вы смешиваете интерфейс и реализацию. Большинство практических значений разделяющего интерфейса и реализации - это способность легко писать макетные объекты для целей тестирования.

В любом случае, кажется, проблема, которую вы пытаетесь решить, - это совместить наследование интерфейса с наследованием реализации на С++. Я бы предложил использовать шаблонный класс для реализации окна.

// Window interface
class LWindow
{
};

// ListBox interface (inherits Window interface)
class LListBox : public LWindow
{
};    

// Window implementation template
template<class Interface>
class WindowImpl : public Interface
{
};

// Window implementation
typedef WindowImpl<LWindow> Window;

// ListBox implementation
// (inherits both Window implementation and Window interface)
class ListBox : public WindowImpl<LListBox>
{
};

Как я помню, библиотека Windows WTL основана на аналогичной схеме объединения интерфейсов и реализаций. Надеюсь, это поможет.

Ответ 5

О, человек, это путает.

ОК, поэтому L *** - это иерархия интерфейсов, это прекрасно. Теперь, что вы используете p_Impl для, если у вас есть интерфейс, почему вы включили в него реализацию?

Макрос, конечно, некрасивый, плюс его обычно невозможно сделать. Все дело в том, что у вас будут разные реализации, если вы этого не сделаете, то зачем создавать несколько классов в первую очередь?

Ответ 6

OP кажется смущенным. Здесь "что делать, это очень сложно, но оно работает".

Правило 1: Создайте абстракции. Если у вас есть отношение "is-A", вы должны использовать общедоступное виртуальное наследование.

struct Window { .. };
struct ListBox : virtual Window { .. };

Правило 2: Создавайте реализации, если вы реализуете абстракцию, вы должны использовать виртуальное наследование. Вы можете использовать наследование для сохранения дублирования.

class WindowImpl : virtual Window { .. };
class BasicListBoxImpl : virtual ListBox, public WindowImpl { .. };
class FancyListBoxImpl : public BasicListBoxImpl { };

Поэтому вы должны прочитать "virtual", чтобы означать "isa", а другое наследование просто экономит на методах перезаписи.

Правило 3: попробуйте убедиться, что в конкретном типе есть только одна полезная функция: конструктор. Иногда это сложно, вам могут потребоваться некоторые настройки по умолчанию и некоторые заданные методы, чтобы что-то менять. После того как объект настроен , удалите реализацию. В идеале вы делаете это при построении:

ListBox *p = new FancyListBoxImpl (.....);

Примечания: вы не собираетесь называть абстрактные методы непосредственно на или в реализации, поэтому частное наследование абстрактной базы просто отлично. Ваша задача состоит исключительно в том, чтобы определять эти методы, а не использовать их: для клиентов только абстракций. Реализации виртуальных методов с баз также могут быть также закрытыми по той же причине. Наследование для повторного использования, вероятно, будет общедоступным, поскольку вы можете использовать эти методы в производном классе или вне его после построения, чтобы настроить свой объект, прежде чем отбрасывать детали реализации.

Правило 4. Существует стандартная реализация для многих абстракций, известных как делегирование, о которой вы говорили:

struct Abstract { virtual void method()=0; };

struct AbstractImpl_Delegate: virtual Abstract { 
  Abstract *p;
  AbstractImpl_Delegate (Abstract *q) : p(q) {}
  void method () { p->method(); }
};

Это милая реализация, поскольку она не требует, чтобы вы знали что-либо об абстракции или о том, как ее реализовать...:)

Ответ 7

Я обнаружил, что

Использование препроцессор #define. определить константы не так точно.

[src]

Макросы, по-видимому, не такие точные, я даже не знал, что... Классические скрытые опасности препроцессора типа:

#define PI_PLUS_ONE (3.14 + 1)`

Таким образом, вы избегаете возможности что порядок операций будет уничтожить смысл вашей константы:

x = PI_PLUS_ONE * 5;`

Без в скобках преобразован в

x = 3.14 + 1 * 5;

[src]