Обновить
154
Павел Остапенко@mt_

Пользователь

2
Подписчики
Отправить сообщение
На мой взгляд, проблема здесь в том, что, приступая к программе, следует задаться двумя вопросами (о них есть у Голуба [2]):
1) Какие возможности должно поддерживать это приложение?
2) Как реализовать эти возможности простейшим способом?

С моей точки зрения, неверно писать приложение на все случаи жизни. В пределе, мы получим сумасшедший интерфейс в стиле M$ OLE/COM, который пытается быть интерфейсом-на-все-случаи-жизни, но, в результате, ни одну проблему не решает простым и надёжным способом.

Да, ваш класс привязан к ГУИ. И это правильно! Потому что он работает с ГУИ. Данным конкретным ГУИ. В данной конкретной программе. Вы решаете конкретную задачу, которая есть в данной конкретной работе.

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

Мой совет. Смотрите на форматы doc-файлов. Или на интерфейсы типа ОЛЕ. Путь вас передёрнет, пусть это будет отличной прививкой от решения неуместно общих задач в конкретном приложении.

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

Разумеется — это моё мнение.
Спасибо за конструктивные комментарии, они мотивируют.

MVC — не священная корова. Кроме того, я бы не стал говорить, что MVC — это однозначное «раздербанивание» цельного класса на три более мелких, связанных жёстким интерфейсом. Моё понимание MVC — это, скорее, логическое разделение на три группы. Например, функций-членов. В моём случае, оно скорее влияет на внутреннюю структуру класса, чем на иерархию классов.

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

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

Всё равно тот же самый функционал (ядро + визуализация) в программе есть. Только он разнесён по классам. Фактически, то, что я между ними «гоняю» туда-сюда, должно быть внутренним содержимым. Чёрт с ним, попробую сделать в одном классе. Пусть он будет уметь всё сам. И что? Да, я меняю кое-что в ходе программы, но огромных интерфейсов, где я обновляю состояние объекта по 25 контролам (либо одной функцией, либо кучей других — неважно) — их больше нет, понимаете?
>> Если вы представляете себе цикл разработки крупных игровых проектов…
> Я представляю себе цикл разработки крупных неигровых проектов!

ru.wikipedia.org/wiki/%D0%9B%D0%BE%D0%B3%D0%B8%D0%BA%D0%B0
К чему столько негатива? Вы написали несколько неплохих казуальных игр. Это замечательно.
Насколько я могу судить по скриншотам, мой код использовался в тайтле класса ААА даже после моего ухода из команды во 2 и 3 частях игры, в течение минимум нескольких лет. Если вы представляете себе цикл разработки крупных игровых проектов, вы должны знать что это означает.

Предлагаю не судить по т.н. «уровню» только исходя из того, что автор предлагает подходы, отличающиеся от ваших. Это первое.

Второе. Нигде не пишу, что автор этих подходов — я. Да, я их использую, я набил шишки, за моими текстами мои бессонные ночи и набитые шишки. Как написал автор фреймворка, придумавший подход, о котором я буду рассказывать в следующей статье, «переизобретение велосипеда, возможно, имеет смысл, если этот велосипед в несколько раз быстрее прочих».

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

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

Если требуется динамическая взаимозаменяемость интерфейса, продумал бы общий абстрактный интерфейс, три класса интерфейсов (веб, ГУИ, консоль) отвёл бы от него, а мой общий класс работал бы с указателем на одну из реализаций.
Что конкретно предлагаю в вашем случае:
1) Отказаться от Сет() функции.
2) КомбоБокс перенести внутрь класса, если позволяет фреймворк. Если не позволяет, т.е. КомбоБокс автоматически является членом класса Окна, связать ваш класс с указателем на КомбоБокс так, как Вы это показали.
3) Вся работа с КомбоБокс — внутри класса. В частности: установка начального значения контрола, а также привязывание колбэков КомбоБокса к функциям-членам класса, чтобы класс внутри себя обрабатывал нажатия пользователя.
Благодаря такому подходу, вся работа с конкретным контролом будет внутри класса. Там, где располагаются внутренние переменные, а значит нет необходимости городить лишние открытые интерфейсы для обмена данными.
За ссылку спасибо, почитаю на досуге. Тогда смогу что-то сказать.
В нашем случае, действительно, была структура, построенная на сущностях. Но главный программист, которого я считаю очень талантливым человеком, реализовал свойства сущностей динамически, кроме того, к ним применялось позднее связывание. Это, конечно, решало ряд проблем, хотя и усложняло код. Возможно, с тех пор придумали что-то лучше — буду смотреть.
1. Это бы отлично работало, если бы каждое изменение функционала сущности не пришлось бы отслеживать в остальных классах. Но вполне допускаю, что в сложных случаях я бы разбил функционал по разным файлам, минимизируя сцепки между ними за счёт обобщения интерфейсов. Если уж сильно хочется разнести по файлам — возможно, я бы разнёс реализацию функций-членов класса, чем создавал лишние сущности в виде новых классов. Предлагаю этот пункт перевести на конкретные примеры.

2. :)

3. Отлично дружит. Приведите коротко конкретный пример класса, который будет иметь несколько представлений, я покажу как подружить его с правильной инкапсуляцией.
«Если с сущностью выполняется много операций — то интерфейс класса превратиться в мясо»
— если операции за сущность будут работать другие сущности, это будет ещё большее мясо, разнесённое, к тому же, по разным файлам, что ещё больше затрудняет прочтение кода.

«Поэтому сериализацию и подобные операции имеет смысл разнести как минимум по разным интерфейсам, а порой и по вспомогательным классам.»
— Что мешает спрятать сериализацию в классе? void Entity::Serialize(Stream *);
Всё отлично работает. Если сериализацию делает внешний код, в него требуется передать всё содержимое объекта. А зачем? Проще хранить внутри.

«А насчет «выведи себя в интерфейс» — по моему мнению вообще за гранью добра и зла.»
— Подумайте, пожалуйста, об этом:
habrahabr.ru/blogs/cpp/111120/#comment_3543396
Что касается продуктивности — вопрос спорный. Готов обсудить подробнее, на тех примерах, которые Вы приведёте. Доводы в защиту моего подхода я привёл в самой статьи.
Что касается оптимизации — здесь согласен на 100%. На эту тему я даже писал статью. Но, согласитесь, это немного иной вопрос, выходящий за рамки введения в методики программирования.
Как показывают комментарии к статье, она заставила многих задуматься о том, как они применяли инкапсуляцию до этого. На эту тему уже несколько веток обсуждения.
Я бы с радостью добавил ещё материала, но, видимо, правильно не стал. Нам бы с этим пунктом пока разобраться. Если Вы имеете практический опыт по правильно реализованной инкапсуляции, пожалуйста, помогите с комментариями. Их слишком много, я не успеваю всем отвечать.
Вы взяли отличный классический пример. Он поможет очень быстро объяснить суть идеи.
Давайте задумаемся. Задумаемся как профессионалы — практики. Зачем в чистом абстрактном классе координаты? Почему Вы заставляете всех наследников Фигуры иметь математическое описание через точку на декартовой плоскости? Подумайте об этом искусственном ограничении, которым Вы сами себя связали.
Что Вы хотите на самом деле? Вы хотите, чтобы фигура выводилась в указанным координатам. Что это? Правильно, это интерфейс, то есть класс с чисто виртуальными функциями-членами:
class Shape
{
public:
virtual void Paint(int x, int y) = 0;
};


А что если мой объект — сплайн, описываемый вещественными уравнениями? Ему не надо навязывать внутреннюю структуру, завязанную на паре целочисленных координат. У него вообще нет такого понятия как «координата центра».

Мораль такова: не нужно создавать лишнее сцепление там, где оно не нужно. Объект сам отлично разберётся что и как ему иметь внутри. Ваша задача — продумать хороший общий интерфейс.

И последнее. Наследование само по себе — это довольно сильное сцепление. Лучше проектировать сравнительно «широкую» иерархию классов, чем делать наследование там, где без него можно обойтись.
В случае разделения логики и управления интерфейсом, Вам приходится «размазывать» семантику класса по ряду других объектов. Это увеличивает код за счёт интерфейсов-сцепок. Зачастую это также разносит логически близкий функционал по разным файлам, что затрудняет чтение кода. И, наконец, когда Вы будете что-либо менять в своей программе, Вам придётся менять не только содержимое классов логики и интерфейса, но и интерфейс между ними. Почти всегда это можно организовать в одном классе, в одном файле спп.
Ни в коем случае не навязываю свой подход, но подумайте, взвесьте ещё раз все «за» и «против» — возможно, объединение логики с интерфейсом в едином классе, не так уж плохо, особенно когда речь идёт о средних и небольших интерфейсах пользователя.
По первому пункту. Вы приводите канонический подход, и он, безусловно, правилен. В моём случае, я привожу более простое мнемоническое правило, которое, хотя и не так красиво звучит, но включает и ваш подход, и позволяет обрабатывать случаи, когда взаимосвязь «содержит», а не «является», но экземпляров содержимого может быть только один.

Вы правы в том что касается сумбурности и краткости. Слишком много сложного материала дальше. Приходится делать введение кратким. Ни в коем случае не пытаюсь заменить собой классические книги, приведённые в списке литературы. Моё дело — заинтересовать, вызвать желание разобраться. Желающие смогут разобраться «с карандашом» с моей статьёй и книгами, либо задать вопросы в комментариях.

За ценное замечание — спасибо.
В статье я говорю о передаче моего опыта и моих наработок. Отдаю себе отчёт, что множество других людей (наверняка часть из них умнее и опытнее меня) пишет иначе. В статье я решил поделиться тем, что имею. Потому что оно работает и неплохо себя зарекомендовало. Так или иначе, автор ещё студентом работал в одной из ведущих гейм-девелоперских студий страны, а потом заменил собой почти весь штатный отдел программистов в одной из лидирующих компаний — автоматизаторов мелких и средних предприятий. Возможно, этот опыт будет кому-то здесь полезен.
Вы совершенно правы, это азбука. Именно с азбуки я и начал цикл статей. Потому что более сложные вопросы будут базироваться на изложенном здесь.
Приношу извинения, если статья правда сумбурная.
Я, правда, рассчитывал на внимательное и «с карандашом» её изучение.

Что касается следующей части, обязательно постараюсь учесть Ваши комментарии. Буду признателен Вашим конкретным предложениям по упорядочению данной и последующих статей.
Что ценного я лично вижу в своей статье:
1) Напоминаю об делении на куски и отладке по кускам. TDD — лишь один из вариантов тестирования этих кусков, который, конечно, имеет право на существование. Я специально нигде не навязываю стиль создания этих элементов, потому что каждому ближе своё, всё зависит от навыков и специфики проекта. Мне, например, ближе тестирование по граничным условиям, а потом уже переход к автоматизированным тестам, по необходимости. Это позволяет идти от задачи и экономить время, так как я не делаю развёрнутое тестирование там, где оно, в ходе отладки этюда, становится ненужным.
2) Рассказываю о важности переносить сложные алгоритмы на бумагу и отлаживать их на бумаге, для начала — в простом русском языке.
3) Напоминаю о простой методике создания структуры классов.
4) Предлагаю использовать инкапсуляцию *правильно*, объясняю почему так следует делать на жизненных примерах.
5) Заранее готовлю начинающих к тому, что переписывать с нуля — это нормально.

Для вводной статьи, по-моему, вполне достаточно. Обсуждения выше и ниже показывают, что не стоило добавлять ничего больше, чтобы не мешать обсуждение самого сложного момента статьи — инкапсуляции — с другими темами.
Это подтверждается, если мы подумаем о времени жизни контрола: она должна совпадать с временем жизни родительского объекта, а не класса.
-->
Это подтверждается, если мы подумаем о времени жизни контрола: она должна совпадать с временем жизни родительского объекта, а не объекта окна.

Информация

В рейтинге
Не участвует
Откуда
Москва и Московская обл., Россия
Зарегистрирован
Активность

Специализация

Технический директор
Оптимизация бизнес-процессов
Управление разработкой
Наставничество
Fullstack
Agile