Порой приходится слышать, что "C++ не поддерживает инкапсуляцию". Не знаю как такое может быть. Но мое мнение, сам программист не может/хочет обеспечить инкапсуляцию своих объектов в программе, прикрываясь подобными высказываниями.
Самое забавное, конечно, что я слышал по поводу инкапсуляции и C++: "Раз в C++ нет нативной поддержки свойств, значит там нет нормальной инкапсуляции". Ну что ж, будем терпимы к другим. В конце концов сколько людей, столько и мнений.
В этой публикации я покажу, как при помощи нескольких нехитрых, я бы сказал тривиальных, приемов встать на истинный путь инкапсуляции. Конечно, на C++.
Закрытые поля
Это самое первое и необходимое, что нужно сделать в типе при его описании. Если будет открыто хотя бы одно поле (модификатором public), то считайте, что инкапсуляции у вас в объектах этого типа нет. Обеспечить доступ к закрытым полям можно тривиально:
Конструктор копирования
Когда имеешь дело с примитивными типами, возможно, дело закрытыми полями обойдется. Но все меняется, когда одни классы вашего приложения начинают пользовать другие классы. Например,
Внимательные читатели, например, как один из моих коллег, уже нашли серьезную недоработку в приведенном примере. Наконец-то мне удалось найти время исправить ее.
Друзья
Не используйте friend в своих классах, потому что это прекрасный способ избежать инкапсуляции. Неизвестно, что дружеского может натворить другой класс с вашими закрытыми полями.
Сторонники friend конечно могут пожаловаться, что получение некоторых свойств объектов через вызов функций может привести к падению производительности, но говорить без замеров беспредметно. К тому же некоторые методы объектов можно делать inline. Это дизайну не вредит.
Порой встречаются довольно таки интересные варианты использования ключевого слова friend. Например, как, если можно сказать, аналог partial C#. То есть описание по сути одного класса в двух файлах. В таком случае скорее всего мы имеем дело с плохим дизайном. Лучше увеличить количество типов в программе, чем вот так вот "подружиться".
Зачем все это нужно?
Инкапсуляция позволяет улучшить поддержку проекта. Это достоинство очерчивает и область применения инкапсуляции. Если вы уверены, что проект, над которым вы работаете, будет жить долго, и, возможно, в нем придется внедрять еще не одну новую функциональность, то инкапсуляция - это то, что вам нужно в первую очередь.
Есть практики, которые пренебрегают инкапсуляцией. Например, Data Oriented Design. По этому поводу ведутся споры, но ясно одно: поддержка таким проектам не грозит - у них краткий срок использования и продаж.
Если все ваши объекты будут контролировать свое состояние, то программа будет легкочитаемой и более понятной. Кроме того, использование инкапсуляции позволит локализовать ошибки в проекте в будущем.
Ну и наконец, инкапсуляция - необходимое условие тестируемости типов.
Наверняка, это не полный список чего можно сделать, чтобы гарантировать целостность объектов в вашем приложении. Но определенные шаги в этом направлении, я надеюсь, сделать вам поможет.
Самое забавное, конечно, что я слышал по поводу инкапсуляции и C++: "Раз в C++ нет нативной поддержки свойств, значит там нет нормальной инкапсуляции". Ну что ж, будем терпимы к другим. В конце концов сколько людей, столько и мнений.
В этой публикации я покажу, как при помощи нескольких нехитрых, я бы сказал тривиальных, приемов встать на истинный путь инкапсуляции. Конечно, на C++.
Закрытые поля
Это самое первое и необходимое, что нужно сделать в типе при его описании. Если будет открыто хотя бы одно поле (модификатором public), то считайте, что инкапсуляции у вас в объектах этого типа нет. Обеспечить доступ к закрытым полям можно тривиально:
Если нужно модифицировать приватное/защищенное поле объекта, создайте соответствующий метод:
- class Foo
- {
- public:
- Foo(INT count)
- {
- _count = new INT(count);
- }
- INT GetCount()
- {
- return *_count;
- }
- ~Foo()
- {
- delete _count;
- }
- private :
- INT * _count;
- };
* This source code was highlighted with Source Code Highlighter.
Пусть получится больше кода. Да, в примере получилась замена для свойства, и теперь дофига кода и короче сделать просто поле открытым. Однако, представьте, сколько можно сэкономить на психотерапевтах, когда тип разрастется и поддерживать корректное состояние объекта просто так уже нельзя будет.
- class Foo
- {
- public:
- Foo(INT count)
- {
- _count = new INT(count);
- }
- INT GetCount()
- {
- return *_count;
- }
- void SetCount(INT count)
- {
- *_count = count;
- }
- ~Foo()
- {
- delete _count;
- }
- private :
- INT * _count;
- };
* This source code was highlighted with Source Code Highlighter.
Конструктор копирования
Когда имеешь дело с примитивными типами, возможно, дело закрытыми полями обойдется. Но все меняется, когда одни классы вашего приложения начинают пользовать другие классы. Например,
На первый взгляд все впорядке, ведь все поля закрыты. На самом деле можно легко модифицировать foo._inc->_incCount.
- class IncFoo
- {
- public:
- IncFoo(INT incCount) { _incCount = new INT(incCount); }
- void SetIncCount(INT incCount) { *_incCount = incCount; }
- INT GetIncCount() { return *_incCount; }
- ~IncFoo() { delete _incCount; }
- private:
- INT *_incCount;
- };
- class Foo
- {
- public:
- Foo(INT count)
- {
- _count = new INT(count);
- _inc = new IncFoo(count);
- }
- INT GetCount() { return *_count; }
- void SetCount(INT count) { *_count = count; }
- IncFoo GetInc() { return *_inc; }
- ~Foo()
- {
- delete _count;
- delete _inc;
- }
- private :
- INT * _count;
- IncFoo * _inc;
- };
* This source code was highlighted with Source Code Highlighter.
В примере inc будет содержать указатель на тот же incCount, что и foo._inc. Таким образом, в пятой строчке значение foo._inc->incCount изменится на 6. Плюс конкретно в этом примере получаем проблемы с памятью. А дело все в том, что автоматически сгенерированный конструктор копирования для типа IncFoo просто копирует исходный объект как он есть. Если назначить конструктор копирования следующим образом, то проблема решится:
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo(5);
- IncFoo inc = foo.GetInc();
- inc.SetIncCount(6);
- return 0;
- }
* This source code was highlighted with Source Code Highlighter.
Upd 13.01.2012. Оператор присваивания
- class IncFoo
- {
- public:
- IncFoo(INT incCount) { _incCount = new INT(incCount); }
- // Конструктор копирования
- IncFoo(const IncFoo &from) { _incCount = new INT(* from._incCount); }
- void SetIncCount(INT incCount) { *_incCount = incCount; }
- INT GetIncCount() { return *_incCount; }
- ~IncFoo() { delete _incCount; }
- private:
- INT *_incCount;
- };
* This source code was highlighted with Source Code Highlighter.
Внимательные читатели, например, как один из моих коллег, уже нашли серьезную недоработку в приведенном примере. Наконец-то мне удалось найти время исправить ее.
Например, такой код приводит к освобождению одного и того же участка памяти. Дело в том, что foo.GetInc() возвратит объект IncFoo, но с указателем на ту же память, что и foo._inc. Переменной inc и будет присвоен именно этот объект. Для того, чтобы объекты inc._incCount и foo._inc._incCount не указывали на один участок памяти, необходимо определить оператор присваивания для IncFoo
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo(3);
- IncFoo inc(5);
- inc = foo.GetInc();
- inc.SetCount(9);
- return 0;
- }
* This source code was highlighted with Source Code Highlighter.
Собственно, закрытые поля, конструктор копирования и оператор присваивания должны быть выполнены и в классе Foo. Думаю, читатель уже знает, как это сделать
- class IncFoo
- {
- public:
- ....
- IncFoo operator = (const IncFoo &from) { return IncFoo(* from._incCount); }
- ....
- };
* This source code was highlighted with Source Code Highlighter.
Друзья
Не используйте friend в своих классах, потому что это прекрасный способ избежать инкапсуляции. Неизвестно, что дружеского может натворить другой класс с вашими закрытыми полями.
Сторонники friend конечно могут пожаловаться, что получение некоторых свойств объектов через вызов функций может привести к падению производительности, но говорить без замеров беспредметно. К тому же некоторые методы объектов можно делать inline. Это дизайну не вредит.
Порой встречаются довольно таки интересные варианты использования ключевого слова friend. Например, как, если можно сказать, аналог partial C#. То есть описание по сути одного класса в двух файлах. В таком случае скорее всего мы имеем дело с плохим дизайном. Лучше увеличить количество типов в программе, чем вот так вот "подружиться".
Зачем все это нужно?
Инкапсуляция позволяет улучшить поддержку проекта. Это достоинство очерчивает и область применения инкапсуляции. Если вы уверены, что проект, над которым вы работаете, будет жить долго, и, возможно, в нем придется внедрять еще не одну новую функциональность, то инкапсуляция - это то, что вам нужно в первую очередь.
Есть практики, которые пренебрегают инкапсуляцией. Например, Data Oriented Design. По этому поводу ведутся споры, но ясно одно: поддержка таким проектам не грозит - у них краткий срок использования и продаж.
Если все ваши объекты будут контролировать свое состояние, то программа будет легкочитаемой и более понятной. Кроме того, использование инкапсуляции позволит локализовать ошибки в проекте в будущем.
Ну и наконец, инкапсуляция - необходимое условие тестируемости типов.
Наверняка, это не полный список чего можно сделать, чтобы гарантировать целостность объектов в вашем приложении. Но определенные шаги в этом направлении, я надеюсь, сделать вам поможет.
Я так понимаю, сутью инкапсуляции является сокрытие внутренней реализации. Тогда вопрос: Зачем позволять клиенту вот такое
ОтветитьУдалитьIncFoo GetInc() { return *_inc; }
Допустим, у нас есть объект типа А, у него есть метод МетодА. Есть объект Б, он инкапсулирует А. то есть что то типа
класс Б
{
Приватная ссылка А.
}
И допустим, нам понадобилась логика работы с элементом А, посредством элемента Б.
Плохое решение:
класс Б
{
Приватная ссылка А.
Публичный метод Получить копию А
}
Решение лучше
класс Б
{
Приватная ссылка А.
Публичный метод "Вызвать Метод А"
{
А.МетодА
}
}
Такое решение зависит от многого. В том числе от поставленной задачи. Может быть лучше и так и так.
ОтветитьУдалитьНе лучше Get/Set методы помечать inline?
ОтветитьУдалитьОднозначно лучше. Но все по очереди.
УдалитьНадеюсь, меня никто не вынудит написать пост про inline. Я серьезно.