понедельник, 19 декабря 2011 г.

Инкапсуляция в C++

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

Закрытые поля
Это самое первое и необходимое, что нужно сделать в типе при его описании. Если будет открыто хотя бы одно поле (модификатором public), то считайте, что инкапсуляции у вас в объектах этого типа нет. Обеспечить доступ к закрытым полям можно тривиально:

  1. class Foo
  2. {
  3. public:
  4.   Foo(INT count)
  5.   {
  6.     _count = new INT(count);
  7.   }
  8.   INT GetCount()
  9.   {
  10.     return *_count;
  11.   }
  12.     ~Foo()
  13.   {
  14.     delete _count;
  15.   }
  16. private :
  17.   INT * _count;
  18. };
* This source code was highlighted with Source Code Highlighter.
Если нужно модифицировать приватное/защищенное поле объекта, создайте соответствующий метод:

  1. class Foo
  2. {
  3. public:
  4.   Foo(INT count)
  5.   {
  6.     _count = new INT(count);
  7.   }
  8.   INT GetCount()
  9.   {
  10.     return *_count;
  11.   }
  12.   void SetCount(INT count)
  13.   {
  14.     *_count = count;
  15.   }
  16.   ~Foo()
  17.   {
  18.     delete _count;
  19.   }
  20. private :
  21.   INT * _count;
  22. };
* This source code was highlighted with Source Code Highlighter.
Пусть получится больше кода. Да, в примере получилась замена для свойства, и теперь дофига кода и короче сделать просто поле открытым. Однако, представьте, сколько можно сэкономить на психотерапевтах, когда тип разрастется и поддерживать корректное состояние объекта просто так уже нельзя будет.

Конструктор копирования
Когда имеешь дело с примитивными типами, возможно, дело закрытыми полями обойдется. Но все меняется, когда одни классы вашего приложения начинают пользовать другие классы. Например,

  1. class IncFoo
  2. {
  3. public:
  4.  IncFoo(INT incCount) { _incCount = new INT(incCount); }
  5.  void SetIncCount(INT incCount) { *_incCount = incCount; }
  6.  INT GetIncCount() { return *_incCount; }
  7.  ~IncFoo() { delete _incCount; }
  8. private:
  9.  INT *_incCount;
  10. };
  11. class Foo
  12. {
  13. public:
  14.  Foo(INT count)
  15.  {
  16.   _count = new INT(count);
  17.   _inc = new IncFoo(count);
  18.  }
  19.  INT GetCount() { return *_count; }
  20.  void SetCount(INT count) { *_count = count; }
  21.  IncFoo GetInc() { return *_inc; }
  22.  ~Foo()
  23.  {
  24.   delete _count;
  25.   delete _inc;
  26.  }
  27. private :
  28.  INT * _count;
  29.  IncFoo * _inc;
  30. };
* This source code was highlighted with Source Code Highlighter.
На первый взгляд все впорядке, ведь все поля закрыты. На самом деле можно легко модифицировать foo._inc->_incCount.

  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3.   Foo foo(5);
  4.   IncFoo inc = foo.GetInc();
  5.   inc.SetIncCount(6);
  6.   return 0;
  7. }
* This source code was highlighted with Source Code Highlighter.
В примере inc будет содержать указатель на тот же incCount, что и foo._inc. Таким образом, в пятой строчке значение foo._inc->incCount изменится на 6. Плюс конкретно в этом примере получаем проблемы с памятью. А дело все в том, что автоматически сгенерированный конструктор копирования для типа IncFoo просто копирует исходный объект как он есть. Если назначить конструктор копирования следующим образом, то проблема решится:

  1. class IncFoo
  2. {
  3. public:
  4.  IncFoo(INT incCount) { _incCount = new INT(incCount); }
  5.  // Конструктор копирования
  6.  IncFoo(const IncFoo &from) { _incCount = new INT(* from._incCount); }
  7.  void SetIncCount(INT incCount) { *_incCount = incCount; }
  8.  INT GetIncCount() { return *_incCount; }
  9.  ~IncFoo() { delete _incCount; }
  10. private:
  11.  INT *_incCount;
  12. };
* This source code was highlighted with Source Code Highlighter.
Upd 13.01.2012. Оператор присваивания
Внимательные читатели, например, как один из моих коллег, уже нашли серьезную недоработку в приведенном примере. Наконец-то мне удалось найти время исправить ее.

  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3.   Foo foo(3);
  4.   IncFoo inc(5);
  5.   inc = foo.GetInc();
  6.   inc.SetCount(9);
  7.   return 0;
  8. }
* This source code was highlighted with Source Code Highlighter.
Например, такой код приводит к освобождению одного и того же участка памяти. Дело в том, что foo.GetInc() возвратит объект IncFoo, но с указателем на ту же память, что и foo._inc. Переменной inc и будет присвоен именно этот объект. Для того, чтобы объекты inc._incCount и foo._inc._incCount не указывали на один участок памяти, необходимо определить оператор присваивания для IncFoo

  1. class IncFoo
  2. {
  3. public:
  4.   ....
  5.   IncFoo operator = (const IncFoo &from) { return IncFoo(* from._incCount); }
  6.   ....
  7. };
* This source code was highlighted with Source Code Highlighter.
Собственно, закрытые поля, конструктор копирования и оператор присваивания должны быть выполнены и в классе Foo. Думаю, читатель уже знает, как это сделать
Друзья
Не используйте friend в своих классах, потому что это прекрасный способ избежать инкапсуляции. Неизвестно, что дружеского может натворить другой класс с вашими закрытыми полями.
Сторонники friend конечно могут пожаловаться, что получение некоторых свойств объектов через вызов функций может привести к падению производительности, но говорить без замеров беспредметно. К тому же некоторые методы объектов можно делать inline. Это дизайну не вредит.
Порой встречаются довольно таки интересные варианты использования ключевого слова friend. Например, как, если можно сказать, аналог partial C#. То есть описание по сути одного класса в двух файлах. В таком случае скорее всего мы имеем дело с плохим дизайном. Лучше увеличить количество типов в программе, чем вот так вот "подружиться".


Зачем все это нужно?
Инкапсуляция позволяет улучшить поддержку проекта. Это достоинство очерчивает и область применения инкапсуляции. Если вы уверены, что проект, над которым вы работаете, будет жить долго, и, возможно, в нем придется внедрять еще не одну новую функциональность, то инкапсуляция - это то, что вам нужно в первую очередь.
Есть практики, которые пренебрегают инкапсуляцией. Например, Data Oriented Design. По этому поводу ведутся споры, но ясно одно: поддержка таким проектам не грозит - у них краткий срок использования и продаж.
Если все ваши объекты будут контролировать свое состояние, то программа будет легкочитаемой и более понятной. Кроме того, использование инкапсуляции позволит локализовать ошибки в проекте в будущем.
Ну и наконец, инкапсуляция - необходимое условие тестируемости типов.
Наверняка, это не полный список чего можно сделать, чтобы гарантировать целостность объектов в вашем приложении. Но определенные шаги в этом направлении, я надеюсь, сделать вам поможет.

4 комментария:

  1. Я так понимаю, сутью инкапсуляции является сокрытие внутренней реализации. Тогда вопрос: Зачем позволять клиенту вот такое
    IncFoo GetInc() { return *_inc; }

    Допустим, у нас есть объект типа А, у него есть метод МетодА. Есть объект Б, он инкапсулирует А. то есть что то типа

    класс Б
    {
    Приватная ссылка А.
    }

    И допустим, нам понадобилась логика работы с элементом А, посредством элемента Б.

    Плохое решение:
    класс Б
    {
    Приватная ссылка А.
    Публичный метод Получить копию А
    }

    Решение лучше

    класс Б
    {
    Приватная ссылка А.
    Публичный метод "Вызвать Метод А"
    {
    А.МетодА
    }
    }

    ОтветитьУдалить
  2. Такое решение зависит от многого. В том числе от поставленной задачи. Может быть лучше и так и так.

    ОтветитьУдалить
  3. Не лучше Get/Set методы помечать inline?

    ОтветитьУдалить
    Ответы
    1. Однозначно лучше. Но все по очереди.
      Надеюсь, меня никто не вынудит написать пост про inline. Я серьезно.

      Удалить