Ответы пользователя по тегу ООП
  • Как архитектурно правильно организовать классы в игре на JS. (ООП)?

    @Mercury13
    Программист на «си с крестами» и не только
    Статические методы класса как раз для оперирования с несколькими экземплярами класса?

    Статические поля и методы служат для работы с классом в целом. Примеры:
    • Есть десять объектов и новых создавать нельзя.
    • Общий для всей проги объектный пул, а второго, скорее всего, не будет.
    • Псевдоконструктор — Rect.xyxy(x1,y1,x2,y2) и Rect.xywh(x,y,w,h).
    • Функциональность никак не зависит от экземпляра объекта: calculateDamageWithCrit при условии, что генератор случайных чисел тоже статический (принадлежит библиотеке языка в целом, а не игровому миру).

    Где правильно хранить экземпляры классов врагов?

    С Wave вы, вероятно, сами запутались: это то информация о волне, то часть текущего состояния мира. Я бы всех врагов закинул в Game (правда, назвал бы объект World).

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

    Башня принадлежит игровому миру — тоже её в Game (или World).

    У башни есть массив с пулями которые она выпускает.

    Пули очень не стоит в башню — их лучше в тот же Game/World. Башню вы уничтожаете — пули тоже исчезают?

    И эта вот функция просчёта у меня просто в файле. Нужно ли её загонять в класс как метод?

    Только для красоты. Не важно, где лежит скрытая функция, если она не зависит от экземпляра (типа-статическая) и скрытая (не видна снаружи).

    Важная штука: часто в играх есть неизменная информация о врагах, башнях, пулях (TowerType, EnemyType, BulletType, WaveInfo…), и есть конкретный экземпляр (Tower, Enemy, Bullet). Иногда враги, башни и пули объединяют в один GameObj, но это уже зависит от архитектуры. Возможно и так, и этак.
    Ответ написан
    6 комментариев
  • Зачем нужен интерфейс, если есть абстрактный класс?

    @Mercury13
    Программист на «си с крестами» и не только
    Разрешите добавить. Интерфейс, грубо говоря,— это абстрактный класс без данных и недописанных функций (каждая или полностью жизнеспособна в какой-то ситуёвине, или нулевая = полностью абстрактная, не путать с пустой). И причины этому две.

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

    2. Техническая. Прикрывает один такой серьёзный жупел, как ромбическое наследование данных. Ромбическое наследование данных бывает двух видов.
    Пусть у нас такое:
    class Grandfather { public: int field; };
    class LeftFather : Grandfather {};
    class RightFather : Grandfather {};
    class Son : LeftFather, RightFather {};

    а) С дублированием, когда у сына два поля field, унаследованное через левого отца и через правого. Достаточно сделать функцию…
    void foo(LeftFather& x) { x.field = 42; }
    Как сохранить синхронизацию для левой и правой ветки наследования?
    б) С общим дедом, когда у сына одно такое поле (через виртуальное наследование Си++). Тут левая и правая ветки могут быть просто не готовы к тому, что поле будет меняться без её ведома.

    Ромбическое наследование функций не так вредно: ведь любая функция, извините, работает с данными. А значит, она или имеет дело с внешними данными (а значит, в обеих ветках сделает одно и то же), или нулевая, или просто комбинация таких же нулевых (InputStream.remainder() { return size() - pos(); }).

    Почему я говорю «в какой-то ситуёвине». Возьмём тот же remainder. Существуют потоки — скажем, закэшированный ввод с внешнего устройства — которые не имеют размера и позиции, но способны иметь остаток. Для каких-то потоков можно написать остаток более эффективный. Но это казуистика.
    Ответ написан
    Комментировать
  • Что лучше window.write(object) или object.write(window)?

    @Mercury13
    Программист на «си с крестами» и не только
    Да, относится — к MVC.
    В большинстве случаев лучше window.write(object).
    Внутренние структуры данных не должны зависеть от интерфейса, а интерфейс — может зависеть от внутренних структур данных.
    Если интерфейс сложный, могут быть какие-то промежуточные околоинтерфейсные объекты — например, чтобы изолировать или повторно использовать какую-нибудь логику. И вот в этих-то околоинтерфейсных структурах, каком-нибудь ComboBuilder, может быть writeTo(comboBox).

    Кроме того, в вебе, которому привычно видеть картину в момент T, а не в динамике, MVC если и есть, то сильно модифицированный.
    Ответ написан
    3 комментария
  • Зачем нужны интерфейсы при реализации внедрения зависимостей?

    @Mercury13
    Программист на «си с крестами» и не только
    Если предположить, что у интерфейса А всего одна реализация…
    1. Если объект А сам ссылается на объект Б — чтобы эти объекты не были единым комком, которые можно втянуть в проект только вместе.
    2. Чтобы ускорить перекомпиляцию при изменениях в объекте А.

    Но у интерфейса может быть и много реализаций, тогда…
    3. Чтобы сделать специальную версию А для модульных тестов объекта Б.
    4. Чтобы проще было расширять код или переносить модуль из проекта в проект: например, один и тот же упаковщик, принимающий на вход абстрактный поток, может работать хоть с файлами, хоть с сетью.
    Ответ написан
    Комментировать
  • Как правильно связывать между собой разные интерфейсы?

    @Mercury13
    Программист на «си с крестами» и не только
    Тут лучше всего использовать шаблонные классы.
    interface Collection <T> {}
    class PeopleCollection implements Collection <Person> {}
    Ответ написан
    Комментировать
  • Каким образом обрабатываются объекты в ООП?

    @Mercury13
    Программист на «си с крестами» и не только
    Объект представляет собой обычную структуру, и из полей, которые неявно прописываются в объекты — один или несколько указателей на таблицы виртуальных методов. (Несколько, если есть множественное наследование.)

    В ООП точно так же, как в процедурном, и делай; дополнительный вопрос — как быть, если в одном списке могут быть объекты разных типов. Например, можно делать std::vector<std::unique_ptr<SomeBase>>.
    Ответ написан
    Комментировать
  • Нормально ли обращаться к методам и свойствам классов через единый сервис?

    @Mercury13
    Программист на «си с крестами» и не только
    С моей колокольни статических языков такая конструкция — избыточная сложность на пустом месте.
    Но есть места, где такая сложность ещё и чем-то оправдана. Например, Qt — содержимое ячейки таблицы…
    QVariant SomeModel::data(
        const QModelIndex &index, int role = Qt::DisplayRole) const override {}

    Для чего оно такое в Qt…
    • в самом начале функции может вычисляться адрес в памяти, где все эти данные находятся, а за ним — длинный switch/case «в зависимости от роли, возьми то-то»;
    • возможно возвращение пустого QVariant, когда надо сказать: «действуй как обычно»;
    • наконец, возможны сложные операции с данными, вроде преобразования в отображаемую форму и сортировки, которые не зависят от специфики модели данных.

    Так что ответ. Смотрите по месту: что вы хотите этой конструкцией сказать и почему более простые не оправданы.
    Ответ написан
  • Восходящее преобразование массива производного класса к родительскому?

    @Mercury13
    Программист на «си с крестами» и не только
    Почему нельзя: если мы объявим ящик бананов ящиком фруктов и положим туда яблоко, он перестанет быть ящиком бананов.
    #include <iostream>
    
    class Vocal {  // интерфейс
    public:
        virtual void shout() = 0;
        virtual ~Vocal() = default;
    };
    
    class Dog : public Vocal {
    public:
        virtual void shout() override { std::cout << "Woof!" << std::endl; }
    };
    
    class Unrelated {};
    
    // ВАРИАНТ 1. Метод выкручивания рук.
    
    void shoutAll_v1(Vocal** x, int n)
    {
        for (int i = 0; i < n; ++i)
            x[i]->shout();
    }
    
    template <class T, int N>
    inline void shoutAll_v1(T* (&x)[N]) {
        // Тупая проверка концепции, ждём Си++20
        static_assert (std::is_base_of<Vocal, T>(), "Need array of Vocal");
        T** xx = x;
        shoutAll_v1(reinterpret_cast<Vocal**>(xx), N);
    }
    
    // ВАРИАНТ 2. Виртуальный полиморфизм.
    
    class Choir { // интерфейс
    public:
        virtual int size() const = 0;
        virtual Vocal& at(size_t i) const = 0;
        virtual ~Choir() = default;
    };
    
    void shoutAll_v2(const Choir& x)
    {
        int sz = x.size();
        for (int i = 0; i <sz; ++i)
            x.at(i).shout();
    }
    
    template <class T>
    class VocalArray : public Choir {
    public:
        template <int N>
        VocalArray(T* (&x)[N]) : fData(x), fSize(N) {}
        int size() const override { return fSize; }
        Vocal& at(size_t i) const override { return *fData[i]; }
    private:
        T** const fData;
        int fSize;
    };
    
    int main()
    {
        Dog* sons[3];
        for(auto& x : sons) {
            x = new Dog;
        }
        //Unrelated* unrel[3];
    
        std::cout << "V1" << std::endl;
        shoutAll_v1(sons);
        //shoutAll_v1(unrel);   не компилируется
    
        std::cout << "V2" << std::endl;
        shoutAll_v2(VocalArray<Dog>(sons));
        //shoutAll_v2(VocalArray<Unrelated>(unrel));  не компилируется
    
        return 0;
    }
    Ответ написан
    4 комментария
  • Почему именно такое отношение между классами ( Trip has Airplane)?

    @Mercury13
    Программист на «си с крестами» и не только
    Очевидно, тут имеется в виду жизнь аэропорта в динамике. То есть не заполнить его данными и замолкнуть, а вести вылеты-прилёты, сажать пассажиров в самолёты и т.д.
    Trip — это маршрут, и в одном самолёте могут ехать несколько маршрутов (например, с посадками, или рейс вообще чартерный и несколько турагентств заполняют самолёт).
    Ответ написан
    Комментировать
  • Почему я не вижу результаты работы метода?

    @Mercury13
    Программист на «си с крестами» и не только
    Потому что сначала output, потом work.
    Ответ написан
  • Порядок вызова конструкторов при наследовании?

    @Mercury13
    Программист на «си с крестами» и не только
    Вызов конструктора Parent считается частью вызова конструктора Child. И он происходит раньше, чем конструирование всех полей, добавленных в Child — и уж тем более до тела Child::Child.
    Ответ написан
    7 комментариев
  • SOLID.LSP + ООП.Полиморфизм = противоречиe?

    @Mercury13
    Программист на «си с крестами» и не только
    LSP предписывает наследникам сохранять поведение (контракт) базового класса.

    Не поведение, а ограничения. LSP разрешает только усиливать требования к себе, и только ослаблять — требования к другим. Поведение же может меняться как хочешь в рамках этих ограничений.

    Например, интерфейс Stream позволяет мультиплексированные потоки (то есть потоки, где мы не можем считать записанное, чтение и запись идёт по разным каналам и никак не связаны друг с другом — например, COM-порты), а какой-нибудь BufferedStream ограничивается только потоками, где мы пишем в какую-нибудь цепочку байтов (например, файл), и читаем из неё же, без мультиплексирования.
    Ответ написан
    2 комментария
  • Почему используют interface a не abstract class?

    @Mercury13
    Программист на «си с крестами» и не только
    И первое, и второе имеет право на жизнь.

    Второе действительно используется чаще: у нас есть готовая или полуготовая кнопка, и надо добавить в неё функциональность Нашей Крутой Кнопки™. К тому же слова вроде Clickable лучше подходят для названий интерфейса, чем Button.
    class Button {
      protected void paint(Canvas aCanvas) {}
    }
    
    class MyButton extends Button {
      @Override
      protected void paint(Canvas aCanvas) {}
    }


    А первое — например, мы хотим с Нашей Крутой Кнопкой™ работать как с кнопкой неизвестной функциональности, которая умеет только нажиматься и говорить, в каком она состоянии.
    interface Button {
      void press();
      boolean state();
      void addListener(ButtonListener x);
    }
    
    class GameObject {
      void paint(Renderer renderer);
    }
    
    class MyButton extends GameObject implements Button {
    }
    
    class FridgeGame implements ButtonListener {  // помните, такая была в «Братьях Пилотах»?
      Button buttons[][] = new MyButton[4][4];  
    }
    Ответ написан
    Комментировать
  • Что значит сокрытие?

    @Mercury13
    Программист на «си с крестами» и не только
    Это значит: должно быть сложно или невозможно вывести объект из «адекватного» состояния (которое называется инвариант класса). Все чувствительные поля при этом прячутся от посторонних глаз. (Разумеется, могут быть «небезопасные» методы, но тогда пользователь сам себе злобный буратино).

    PHP управляет памятью сам (что-то мне кажется, что метод управления памятью там «бросай объект и шут с ним»). Но давайте представим себе, что надо вызывать команду «уничтожить объект», и дальнейшее обращение к освобождённому указателю некорректно. Попробуем сделать объект «указатель множественного владения».

    В каждом из управляемых объектов налаживаем счётчик; при переприсваивании на счётчике будет такая цифра, сколько указателей «смотрят» на объект. Счётчик упадёт до нуля — объект уничтожается. Соответственно, поле управляемого объекта «счётчик» и поле указателя «указатель на объект» скрываются. «Адекватное состояние» я уже описал: «на счётчике будет такая цифра, сколько указателей «смотрят» на объект. Счётчик упадёт до нуля — объект уничтожается».
    Ответ написан
    Комментировать
  • Как разобраться, что происходит в этом заголовочном файле?

    @Mercury13
    Программист на «си с крестами» и не только
    Учи понятие «единица компиляции». Тут, к сожалению, есть и вещи, которые должны быть в CPP-файле, и вещи, которые должны быть в H-файле.

    #pragma pack(1)
    Структуры данных нам нужны «один в один», без байтов заполнения.

    struct FileHeader 
    struct MAPINFO

    Формат BMP. Не забывай, что формат BMP записывается с нижней строки!

    Функция Open читает картинку «один в один», Save пишет «один в один», GetMapInfo и GetFH выдают какие-то заголовки нашего BMP.

    Остаётся GetMap(), который, по идее, должен выдавать матрицу цветов, но реально действует только для 32-битного BMP и никак не инкапсулирует ни ширину-высоту матрицы, ни тот факт, что формат BMP пишется с нижней строки.

    За этот код — тройка с минусом.

    А теперь чего ваш код НЕ поддерживает, но, по идее, должен, чтобы выполнить вашу задачу.
    1. Создание BMP нужного размера с нуля, а не загрузка из файла.
    2. Инкапсулировать матрицу пикселей. Желательно так, чтобы был быстрый доступ к строкам как к буферам в памяти, для простоты переноса информации из старого BMP в новый, на 30×30 пикселей больший.
    3. Если вы ограниченно поддерживаете формат BMP — вылетать с ошибкой, если версия неподдерживаемая (например, не то количество цветов).

    Задача именно своими силами наладить поддержку BMP? А то в Builder’е есть TBitmap.
    Ответ написан
    6 комментариев
  • Зачем в абстрактном базовом классе создавать конструктор?

    @Mercury13
    Программист на «си с крестами» и не только
    Абстрактные классы делят на интерфейсы и частично реализованные. Грань между ними такова:
    • Интерфейс не имеет данных.
    • У интерфейса все неабстрактные виртуальные методы представляют собой или эталонное поведение, или самую частую реализацию. В обоих случаях, если что, их надо не расширять, а переписывать с нуля.

    Так вот, для интерфейсов таких конструкторов, разумеется, не нужно.

    Например, между абстрактным потоком и файлом Win32 может быть такая иерархия: Stream → HandleStream → File. Stream — интерфейс, даже если там есть что-то типа
    // virtual
    unsigned long long Stream::remainder() const { return size() - pos(); }


    HandleStream содержит уже данные (дескриптор Win32), и это уже частично реализованный класс, который крутится вокруг этого дескриптора: в деструкторе вызов CloseHandle, конструктор может принимать дескриптор, полученный каким-то «левым» образом.
    HandleStream::HandleStream(HANDLE aHandle) : fHandle(aHandle) {}
    HandleStream::~HandleStream() { close(); }
    
    void HandleStream::close()
    {
      if (Handle != INVALID_HANDLE)  { // не помню, как там эта константа в Win32
        CloseHandle(fHandle);
        fHandle = INVALID_HANDLE;
      }
    }

    Вот в таких полуреализованных классах, разумеется, конструктор может инициализировать те данные, которые там есть.
    Ответ написан
  • Как уменьшить связанность классов?

    @Mercury13
    Программист на «си с крестами» и не только
    1. Что такое Container и для чего он нужен? Возможно, от этого дела удастся избавиться или заменить интерфейсом?
    2. Не должен конструктор Graph брать в параметры Parser. Наоборот, Parser функцией parse() должен возвращать Graph.
    3. Config стоит разбить на несколько частей: одна специфична для Graph, вторая для Parser. Как их объединять — зависит от того, кому какие настройки нужны.
    Ответ написан
    2 комментария
  • Нужно ли создавать интерфейсы для одного класса?

    @Mercury13
    Программист на «си с крестами» и не только
    1. Если из класса можно вытащить какую-то абстракцию. Например, из объекта «файл» можно вытащить абстракцию «поток». Личное — объект Project реализует интерфейс Modifiable с двумя функциями: modify() и isModified().

    2. Для упрощения юнит-тестирования при условии владения.
    Предположим, у нас есть класс «класс» (школьный) и класс «ученик». Ученик знает, в каком он классе.
    В такой ситуации получается «клубок»: если надо делать ученика, то надо делать и класс.
    Этот замкнутый круг можно разорвать, сделав интерфейс ISchoolClass и унаследовав от него класс. При юнит-тестировании заменяем класс на какую-то заглушку.
    Ответ написан
    Комментировать
  • Как сделать разную реализацию одной и той же функции класса в C++?

    @Mercury13
    Программист на «си с крестами» и не только
    Перенести пользовательскую функциональность в другое место — так называемый «слушатель».
    using EvClick = void (*)();
    
    Class Model{
    public:
      void click() { if (fOnClick) fOnClick(); }
      void setOnClick(EvClick x) { fOnClick = x; }
    private:
      EvClick fOnClick = nullptr;
    }

    Подобные слушатели есть в любой визуальной оконной библиотеке: VCL, Qt. В VCL так и есть, за исключением вписанных в синтаксис свойств. В Qt для этого используют сигналы-слоты.

    Наладить передачу любых данных в эту функцию — шаблон «команда».
    class ClickEvent {
    public:
      int x, y;
      virtual ~ClickEvent();
    }
    
    using EvClick = void (*)(ClickEvent&);
    Ответ написан
    7 комментариев
  • Зачем прописывать методы в Interface когда можно так же в классе?

    @Mercury13
    Программист на «си с крестами» и не только
    Ответ явоспецифичный. Потому что один класс может реализовать сколько угодно интерфейсов, но наследуется лишь от одного класса.

    Ответ концептуальный. Ромбическое наследование. От А наследуются B и C, от них обоих наследуется D.
    1) Если в A есть поле, в D что, это поле будет в двух экземплярах? А если оно protected и в B мы добавили метод, который его меняет?
    2) Если B и C переопределяют какой-то метод foo(), как быть D? А если нужна и версия B.foo(), и C.foo(), и они обе вызывают A.foo — получатеся D.foo вызовет A.foo дважды? А если в C есть второй метод bar(), который вызывает foo() и начинает вести себя не так, как надо, если мы берём реализацию B.foo()?
    В общем, множественное наследование — хорошая штука, но ромбическое — штука опасная. В языке, где любое множественное наследование неизменно ромбическое, всё, что остаётся — делать такие условия, при которых ни 1, ни 2 не сработает.
    Одно из таких условий — унаследоваться от одного класса и нескольких интерфейсов. 1) У интерфейса нет полей, и 2) эталонная реализация, существующая в некоторых языках программирования, в любом случае менее приоритетна, чем конкретная реализация из класса. Вызывать ту и другую нет смысла: если программист написал свою сверх эталонной — значит, он хочет сделать то же другим путём.
    Ответ написан
    Комментировать