Ответы пользователя по тегу ООП
  • Как действительно поможет ООП в реальной программе?

    @majstar_Zubr
    C++, C#, gamedev
    ООП не про локаничный код, а про упрощение внесения изменений, т.к. все области изменений в текстовом виде локализованы в классах, пространствах имён, модулях. Области изменений непосредственно связаны с абстракциями, и чем грамотнее абстракции сформированы, тем быстрее и проще рефакторить код. В общем случае, под абстракциями понимаются не только модели и сущности, которые относятся к задаче, но и связанные с ними процессы,
    Ответ написан
    Комментировать
  • Что такое закон негерметичных абстракций?

    @majstar_Zubr
    C++, C#, gamedev
    Любая абстракция может частично или полностью включать определение другой абстракции.

    Есть какие-то сущности.

    Они взаимодействуют в какой-то среде определённым образом.

    Отразим эти правила в какой-то спецификации.

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

    Это удобно, т.к. если мы увеличиваем количество сущностей, у нас все равно получается без проблем описать все взаимодействия.

    А теперь, допустим, мы хотим решить обратную задачу: мы хотим описать спецификацию концептуально нового взаимодействия. Как этого добиться, если у нас есть набор абстракций, которые привязаны к конкретным сущностям?

    Тут все конкретно зависит от терминологии спецификации, количества уже описанных абстракций и их взаимодействий.

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

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

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

    Следствие закона такое, что идеальный API и идеальная библиотеке наращиваются только композитами с более высокими абстракциями, никогда не имеют depricated методов, никогда не имеют аналогов и никогда не имеют проблем с обратной совместимостью.
    Ответ написан
  • Как правильно перегрузить конструкторы в классе?

    @majstar_Zubr
    C++, C#, gamedev
    Рассмотрим проблему подробнее.

    class A {
    private:
     int aa;
     int ab;
    
    public:
     A()
         : aa(0), ab(0) {}  // (4) note: candidate constructor not viable: requires
                            // 0 arguments, but 1 was provided
     A(int a)
         : aa(a), ab(0) {}  // (2) note: candidate constructor not viable: no known
                            // conversion from 'A' to 'int' for 1st argument
     A(A& obj);  // (3) note: andidate constructor not viable: expects an l-value
                 // for 1st argument
     void Show() { cout << "Var1: " << aa << endl << "Var2: " << ab << endl; }
    };
    
    int main() {
     A obj = 5;  // (1) error: cannot bind non-const lvalue reference of type ‘A&’
                 // to an rvalue of type ‘A’
     obj.Show();
    
     return 0;
    }


    Стандарт обязывает понимать

    A(A& obj);

    как user-defined конструктор копирования. Поэтому, конструктор копирования T::T(const T&) не будет объявлен по-умолчанию компилятором. Со стандарта C++11 предоставляется возможность заставить компилятор генерировать неявно-объявленный конструктор копирования ключевым словом default

    A(A& obj) = default;

    Неявно объявленный конструктор копирования по-умолчанию имеет сигнатуру T::T(const T&) но лишь в случае, когда все базовые классы T имеют конструкторы копирования с параметрами const B& или const volatile B&, и когда все не статические данные-члены класса T имеют конструкторы копирования с параметрами const M& или const volatile M&.

    В ином случае, неявно объявленный конструктор копирования имеет сигнатуру

    T::T(T&)

    В данном случае, конструктор копирования A(A& obj) является тривиальным с сигнатурой сгенерированного неявно-определённого конструктора копирования T::T(T&).
    Тривиальное копирование практически аналогично std::memmove

    В строке ошибки компиляции происходит следующее:

    1) оператор = в строке с ошибкой компиляции не является инициализирующим, поскольку литерал 5 является (rvalue, нельзя взять адрес) const int. Для исполнения операции присваивания компилятор сначала конструирует A(5);

    ...
    A(int a) : aa(a), ab(0) {
     cout << "Copy ctor with 1 parameter is called " << endl;
    }
    …
    int main() {
    …


    Вывод:
    Copy ctor with 1 parameter is called
    Var1: 5
    Var2: 0


    2) операция присваивания имеет дело с A obj = A (5):

    Справа от оператора присваивания находится временный rvalue типа class A.
    Данное присваивание является инициализирующим, что делает его эквивалентным A obj(A(5));
    Для данной операции необходим конструктор с сигнатурой T::T(T&&)
    Это - конструктор перемещения, и он в классе A отсутствует, поскольку неявное определение конструктора перемещения в классе требует отсутствия user-defined конструкторов копирования, оператора = копирования, оператора = присваивания, деструктора. В нашем случае, у нас имеется user-defined A(A& obj);

    Учитывая вышесказанное, для исправления ошибки компиляции можно либо удалить строку
    A(A& obj); ,
    что приведёт к неявному определению компилятором тривиальных конструкторов копирования и перемещения, либо добавить ещё в объявление класса A строку
    A(A&& obj);

    С точки зрения стандарта С++11 и выше, можно утверждать, что выражения A(A& obj); и A(A&& obj); соответственно эквивалентны A(A& obj) = default; и A(A&& obj) = default;
    Ответ написан
    5 комментариев
  • Можно ли сделать индексаторы динамической длиный?

    @majstar_Zubr
    C++, C#, gamedev
    class People{
    List<Person> data = new List<Person>();
    ...


    + обработка ошибок
    Ответ написан
    Комментировать
  • Пользовательские типы и классы это примерно одно и тоже?

    @majstar_Zubr
    C++, C#, gamedev
    Пользовательский тип в узком смысле это составной тип, реализующий некую абстракцию, у которой нет состояния.

    Класс - просто одна из конструкций языка, которая позволяет выражать идеи в рамках парадигмы ООП.

    В широком смысле, пользовательский тип - любой тип, который не определен в компиляторе / интерпретаторе и стандарте языка, а определен пользователем компилятора в исходном коде.

    Разница вот в чем.

    При переходе от процедурной парадигмы к структурной, мы переходим от чтения/записи из памяти к операциями над переменными в некоторых областях видимости. Мы вводим понятие аллокации, инициализации, присваивания, освобождение. Для переменных простых типов эти задачи решаются в компиляторе.

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

    При переходе к ООП, структура неразрывно ассоциируется с некоторой функциональностью и некоторым состоянием. Здесь уже для своего пользовательского типа вручную необходимо описывать конструкцию и деструкцию типа. Некоторые языки второе реализуют через сборщик мусора, но в от в C# есть четкое различение между managed и unmanaged типами, потому что для вторых нужно писать финализатор - технически работающий как деструктор, но как подчеркивает наименование, он им не является, т.к. используется в рантайме сборщиком мусора.

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

    Вкратце о вариантах трактовки пользовательского типа.

    Первый вариант: синтаксический псевдоним. В C++ делается с помощью typedef, например size_t.

    Второй вариант: Plain Data Object. В разных языках наименование варируется, но идея уходит корнями в структуру данных plain old data struct из Си, которая представляет просто участок памяти, в котором подряд идут публичные поля простых интегрированных типов. Например stuct plain_coord { int x; int y; }; Вот тут большая разница между классом, реализующим абстракцию "Координата" и POD: если вы в программе используете несколько систем координат, то класс, реализующий абстракцию, должен инкапсулировать все POD и конвертацию этих POD из одной системы координат в другую.

    Третий вариант: указатели. Конкретно, в C++ в качестве пользовательских типов могут быть
    указатели на функцию (пользователь хочет определить часть сигнатуры функции как тип)

    указатели на функцию-член (более узкие требования, когда эта функция принадлежит какой-то иерархии)

    указатели на составной тип (указатель на POD структуру, указатель на класс)

    указатель на член класса

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

    Пятый вариант: наконец-то Object Reference класс. Но обычно, когда говорят о таких классах, их наоборот, категоризируют не как типы, а как реализации абстракций, потому что с течением времени задача уточняется, набор абстракции становится другим и приходится делать рефакторинг. Соответственно, может поменяться и набор классов, и предметная модель. А про типы тут даже и вспоминать нечего: они есть, они разные, вот и всё.

    Остальные варианты: посмотрите в сторону Haskell, erlang. В целом, у разных языков разные требования к свойствам типов и предполагаемым методам использования типов. В основном это зависит от того, какую на какие парадигмы и на какую модель памяти нацелен язык программирования.

    В языках без строгой типизации можно придерживаться некоторых конвенций и реализовать классы и ООП. Некоторые языки регламентируют средства, которые должны обеспечивать интерпретаторы и компиляторы, чтобы программисты не начали сами реализовать явное управление типами с проверками - от этого драматически страдает производительность, особенно когда вместо типов появляются строковые токены.
    Ответ написан
    Комментировать
  • Как делать сложные запросы используя репозитории и объединение разных таблиц?

    @majstar_Zubr
    C++, C#, gamedev
    Можно использовать представления со стороны БД, либо фабрики хранилищ со стороны кода.
    Модно сначала просто статически запечь в отдельный класс, если это действительно узкое место. Если паттерн себя проявит, то можно оформить в виде фабрики.

    Вон в C# в Entity Framework почти как вы сказали, только там запрос конструируется из коллекций классов, которые умеют в маппинг конкретной предметной области. Но там на выходе уже полноценная ORM с оптимизациями типа отложенного запроса к БД: технология Linq-To-Entity реализует реляционную логику, так что громоздкий SQL запрос модно отложить до самого последнего момента.
    Ответ написан
    Комментировать
  • Как сделать иерархию классов?

    @majstar_Zubr
    C++, C#, gamedev
    Поднимите свойство в класс-родитель.
    Сделайте виртуальные методы в родителе bool Damage( int damagPoints ), bool Heal (int healthPoints).

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

    Если хотите сделать некоторых конкретных Enemy с особыми свойствами, то сделайте реализации инвариантов также виртуальными. Допустим Slug получает 50% урона и только чётный урон.

    Тогда например Slug:

    new public bool Damage( int damagePoints )
    {
        if (isDamageAcceptable(damagePoints))
        {
            int appliedDamage = Math.Ceiling(damagePoints/2);
            TakeDamage(appliedDamage);
        }
        return isDamageAcceptable(damagePoints);
    }
    
     new protected bool isDamageAcceptable( int damagePoints) => (damagePoints > 0) && (0==damagePoints%2) );


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

    В вашем примере вопрос не имеет смысла, так как нет наследования иерархии функциональности. У вас просто какие-то поля с автогенерацией бесполезных методов для чтения и записи полей.

    Если речь о том, что у не у всех задуманных Enemy может быть здоровье, то вам нужно либо менять модель отношения классов и пересмотреть абстракции, либо использовать композицию вместо наследования.
    Ответ написан
    Комментировать
  • Почему переопределение в C# работает так?

    @majstar_Zubr
    C++, C#, gamedev
    public class Class2
        {
            public string GetVal(int a, int b)
            {
                return "base";
            }
    
            public override string ToString()
            {
                return "_Class2_";
            }
        }


    public class Class3 : Class2
        {
    
            
            public string GetVal(int a, int b, bool defaultpar = true)
            {
                return "child with bool";
            }
    
            public new string GetVal(int a, int b)
            {
                return "child";
            }
    
            public override string ToString()
            {
                return "_Class3_";
            }
        }


    class Program
        {
            static void Main(string[] args)
            {
                Class2 obj_reference_c2 = new Class2();
                Class3 obj_reference_c3 = new Class3();
                Class2 obj_reference_assigned = obj_reference_c3;
                Class2 obj_reference_casted = (Class2)obj_reference_c3;
    
                Console.WriteLine($"Вызов - {obj_reference_c2} c параметрами 1, 2 : {obj_reference_c2.GetVal(1, 2)}");
                Console.WriteLine($"Вызов - {obj_reference_c3} c параметрами 1, 2 : {obj_reference_c3.GetVal(1, 2)}");
                Console.WriteLine($"Вызов {obj_reference_c3} c параметрами 1, 2 true: {obj_reference_c3.GetVal(1, 2, true)}");
    
                Console.WriteLine($"Вызов Class3 присвоенного по ссылке типа Class2 - {obj_reference_assigned} c параметрами 1, 2: {obj_reference_assigned.GetVal(1, 2)}");
                Console.WriteLine($"Вызов Class3 приведенного к типу Class2 - {obj_reference_casted} c параметрами 1, 2: {obj_reference_casted.GetVal(1, 2)}");
    
                /*      
                Вариант 1: в Class3 метод public string GetVal(int a, int b) {return "child";} закомментирован
    
                    Вызов - _Class2_ c параметрами 1, 2 : base
                    Вызов - _Class3_ c параметрами 1, 2 : child with bool
                    Вызов _Class3_ c параметрами 1, 2 true: child with bool
                    Вызов Class3 присвоенного по ссылке типа Class2 - _Class3_ c параметрами 1, 2: base
                    Вызов Class3 приведенного к типу Class2 - _Class3_ c параметрами 1, 2: base
    
                 */
    
                /*
                  Вариант 2: в Class3 метод public string GetVal(int a, int b) {return "child";} существует и прячет унаследованный член Class2.GetVal(int,int)
                
                    Вызов - _Class2_ c параметрами 1, 2 : base
                    Вызов - _Class3_ c параметрами 1, 2 : child
                    Вызов _Class3_ c параметрами 1, 2 true: child with bool
                    Вызов Class3 присвоенного по ссылке типа Class2 - _Class3_ c параметрами 1, 2: base
                    Вызов Class3 приведенного к типу Class2 - _Class3_ c параметрами 1, 2: base
    
                    в подобном случае требуется явно указывать в объявлении модификатор new:
                       public new string GetVal(int a, int b) {return "child";}
                 */
                Console.ReadLine();
            }
        }


    В вашем примере 1 не происходит перегрузка метода базового класса.
    Метод базового класса можно перекрыть ( public new string Foo() ) или переопределить, если базовый метод виртуальный.

    Перегруженные методы существуют только в одной области видимости, которая в C# ограничена классами.

    С точки зрения .Net, методы с сигнатурами string _ (int,int) и string _ (int,int,bool) разные, но синтаксически, в случае, если в методе string _ (int,int,bool) третий параметр снабжается параметром по умолчанию, не существует явного способа вызова, кроме Named Arguments.

    public class Class2
        {
            public string GetVal(int a, int b)
            {
                return "base";
            }
    
            public override string ToString()
            {
                return "_Class2_";
            }
        }
    
        public class Class3 : Class2
        {
            public new string GetVal(int a, int b)
            {
                return "child";
            }
    
            public string GetVal(int A, int B, bool defaultpar = true)
            {
                return "child with bool";
            }
    
            public override string ToString()
            {
                return "_Class3_";
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                Class2 obj_reference_c2 = new Class2();
                Class3 obj_reference_c3 = new Class3();
                Class2 obj_reference_assigned = obj_reference_c3;
                Class2 obj_reference_casted = (Class2)obj_reference_c3;
    
                Console.WriteLine($"Вызов - {obj_reference_c2} c параметрами 1, 2 : {obj_reference_c2.GetVal(1, 2)}");
                Console.WriteLine($"Вызов - {obj_reference_c3} c параметрами 1, 2 : {obj_reference_c3.GetVal(1, 2)}");
                Console.WriteLine($"Вызов - {obj_reference_c3} c параметрами 1, 2 : {obj_reference_c3.GetVal(A:1, B:2)}");
                Console.WriteLine($"Вызов {obj_reference_c3} c параметрами 1, 2 true: {obj_reference_c3.GetVal(1, 2, true)}");
    
                Console.WriteLine($"Вызов Class3 присвоенного по ссылке типа Class2 - {obj_reference_assigned} c параметрами 1, 2: {obj_reference_assigned.GetVal(1, 2)}");
                Console.WriteLine($"Вызов Class3 приведенного к типу Class2 - {obj_reference_casted} c параметрами 1, 2: {obj_reference_casted.GetVal(1, 2)}");
    
                /*
                   
                Вариант 1Б: в Class3 метод public string GetVal(int a, int b) {return "child";} закомментирован
    
                    Вызов - _Class2_ c параметрами 1, 2 : base
                    Вызов - _Class3_ c параметрами 1, 2 : child with bool
                    Вызов - _Class3_ c параметрами 1, 2 : child with bool
                    Вызов _Class3_ c параметрами 1, 2 true: child with bool
                    Вызов Class3 присвоенного по ссылке типа Class2 - _Class3_ c параметрами 1, 2: base
                    Вызов Class3 приведенного к типу Class2 - _Class3_ c параметрами 1, 2: base
    
                 */
    
                /*
    
               Вариант 2Б: в Class3 метод public string GetVal(int a, int b) {return "child";} существует и прячет базовый
    
                    Вызов - _Class2_ c параметрами 1, 2 : base
                    Вызов - _Class3_ c параметрами 1, 2 : child
                    Вызов - _Class3_ c параметрами 1, 2 : child with bool
                    Вызов _Class3_ c параметрами 1, 2 true: child with bool
                    Вызов Class3 присвоенного по ссылке типа Class2 - _Class3_ c параметрами 1, 2: base
                    Вызов Class3 приведенного к типу Class2 - _Class3_ c параметрами 1, 2: base
    
                */
    
                Console.ReadLine();
            }
        }


    Данный пример не очень показателен с точки зрения использования named arguments.
    Для этого потребуется несколько аргументов со значениями по умолчанию.

    Хотя я рекомендую никогда не использовать значения по умолчанию, вместо этого стоит делать SOLID классы со специализированными методами и понятным API, а named arguments позволяют фривольно писать код в отрыве от порядка аргументов в сигнатуре, хоть задом наперед: c.GetVal( defaultpar: false, B: 42, A: 777);
    Ответ написан
    1 комментарий
  • Как лучше сформировать класс Record?

    @majstar_Zubr
    C++, C#, gamedev
    Если у вас есть смонения, что в будущем обычным встроенным типом дело не ограничится, то предпочитайте класс.

    Если вы используете концепцию Record для названия, то скорее всего вы ещё не определились с диаграммой классов, потому что Record слишком высокоуровневые понятие, и скорее всего, вы планируете использовать как класс-контейнер, а значит имеет смысл сделать его шаблонным/параметризируемым типом хранения.

    Если все же вы не уверены, то лучше оставить User, потому что так намерения выражены однозначнгти код понятнее, будет проще в будущем рефакторить
    Ответ написан
    Комментировать
  • Почему прямоугольник не является экземпляром класса квадрата?

    @majstar_Zubr
    C++, C#, gamedev
    Дело в том, что в объектно ориентированном программировании обобщаются не понятия, а определённый набор функций, которые совокупно описывают/осуществляют модификацию каких-то логически связанных данных.

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

    С точки зрения ООП, наследование - это перенятие функциональности, но в случае с прямоугольником и квадратом перенятия функциональности нет, потому что площадь прямоугольника по одной стороне нельзя посчитать, а у квадрата нет второй стороны.

    Ромб от квадрата отличается тем, что у квадрат задаётся 1 углом, а ромб двумя.

    Эллипс от окружности тоже отличается сильно, просто окружность в графическом представлении есть частный случай определенной конфигурации данных, которые описывают эллипс. Но. Графическое представление - это уже за пределами ООП.

    ООП - инструмент для программирования в первую очередь, все сущности представляются относительно минимально необходимого обобщения поведения.

    Для площадей, для отрисовки диагоналей и прочих многих функций у квадрата и прямоугольника будут разные сигнатуры и в принципе разные поля, что значит, это разные классы.

    Единственное что у них общее - они оба выпуклые прямоугольники, их можно вписать в окружность. Но если вы не собираетесь реализовывать для своего графического редактора поворот, вписывание в окружность, то не будет у вас причин вводить класс для выпуклых четерёхугольников в принципе.
    Ответ написан
    Комментировать