ООП против макропроблем - PullRequest
       5

ООП против макропроблем

11 голосов
/ 02 декабря 2010

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

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

Система Windows должна работать на нескольких платформах.Предположим на данный момент мы нацелены на Windows и Linux.Для Windows у нас есть реализация интерфейса в LWindow.И у нас есть несколько реализаций для всех LListBox, LComboBox и т. Д. Моя реакция состояла в том, чтобы передать 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: (

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

Ответы [ 7 ]

2 голосов
/ 10 декабря 2010

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

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

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

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

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

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

2 голосов
/ 02 декабря 2010

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

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

2 голосов
/ 02 декабря 2010

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

Отсоедините абстракцию от ее реализации, так что они могут варьироваться независимо.

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

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

1 голос
/ 15 декабря 2010

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

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

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

// 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>
{
};

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

0 голосов
/ 15 декабря 2010

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

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

[источник]

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

#define PI_PLUS_ONE (3.14 + 1)`

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

x = PI_PLUS_ONE * 5;`

Без скобок приведенное вышебудет преобразован в

x = 3.14 + 1 * 5;

[источник]

0 голосов
/ 12 декабря 2010

ОП кажется смущенным.Вот что делать, это очень сложно, но работает.

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

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

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

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

Поэтому вы должны читать «виртуальный», что означает «isa», а другое наследование просто экономит на переписывающих методах.

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

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

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

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

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

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

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

0 голосов
/ 02 декабря 2010

О, боже, это сбивает с толку.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...