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



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

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

Иногда можно столкнуться с критикой в адрес быстродействия ООП-программ. Это правда, незначительный оверхед присутствует, но настолько незначительный, что в большинстве случаев им можно пренебречь в пользу преимуществ. Тем не менее, в узких местах, где в одном потоке должны создаваться или обрабатываться миллионы объектов в секунду, стоит как минимум пересмотреть необходимость ООП, ибо даже минимальный оверхед в таких количествах может ощутимо повлиять на производительность. Профилирование поможет вам зафиксировать разницу и принять решение. В остальных же случаях, скажем, где львиная доля быстродействия упирается в IO, отказ от объектов будет преждевременной оптимизацией.

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

Классы и объекты


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

Самое простое объяснение: класс — это чертеж трансформера, а экземпляры этого класса — конкретные трансформеры, например, Оптимус Прайм или Олег. И хотя они и собраны по одному чертежу, умеют одинаково ходить, трансформироваться и стрелять, они оба обладают собственным уникальным состоянием. Состояние — это ряд меняющихся свойств. Поэтому у двух разных объектов одного класса мы можем наблюдать разное имя, возраст, местоположение, уровень заряда, количество боеприпасов и т. д. Само наличие этих свойств и их типы описываются в классе.

Таким образом, класс — это описание того, какими свойствами и поведением будет обладать объект. А объект — это экземпляр с собственным состоянием этих свойств.

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

Итого, имеем методы и свойства, которые являются атрибутами. Как работать с атрибутами? В большинстве ЯП оператор обращения к атрибуту — это точка (кроме PHP и Perl). Выглядит это примерно вот так (псевдокод):

// объявление класса с помощью ключевого слова class
class Transformer(){
    // объявление поля x
    int x

    // объявление метода конструктора (сюда нам чуть ниже передадут 0)
    function constructor(int x){
        // инициализация поля x 
        // (переданный конструктору 0 превращается в свойство объекта)
        this.x = x
    }
	
    // объявление метода run
    function run(){
        // обращение к собственному атрибуту через this
        this.x += 1
    }
}

// а теперь клиентский код:

// создаем новый экземпляр трансформера с начальной позицией 0
optimus = new Transformer(0)

optimus.run() // приказываем Оптимусу бежать
print optimus.x // выведет 1
optimus.run() // приказывает Оптимусу еще раз бежать
print optimus.x // выведет 2

В картинках я буду использовать такие обозначения:



Я не стал использовать UML-диаграммы, посчитав их недостаточно наглядными, хоть и более гибкими.


Анимация №1

Что мы видим из кода?

1. this — это специальная локальная переменная (внутри методов), которая позволяет объекту обращаться из своих методов к собственным атрибутам. Обращаю внимание, что только к собственным, то бишь, когда трансформер вызывает свой метод, либо меняет собственное состояние. Если снаружи обращение будет выглядеть так: optimus.x, то изнутри, если Оптимус захочет сам обратиться к своему полю x, в его методе обращение будет звучать так: this.x, то есть "я (Оптимус) обращаюсь к своему атрибуту x". В большинстве языков эта переменная называется this, но встречаются и исключения (например, self)

2. constructor — это специальный метод, который автоматически вызывается при создании объекта. Конструктор может принимать любые аргументы, как и любой другой метод. В каждом языке конструктор обозначается своим именем. Где-то это специально зарезервированные имена типа __construct или __init__, а где-то имя конструктора должно совпадать с именем класса. Назначение конструкторов — произвести первоначальную инициализацию объекта, заполнить нужные поля.

3. new — это ключевое слово, которое необходимо использовать для создания нового экземпляра какого-либо класса. В этот момент создается объект и вызывается конструктор. В нашем примере, конструктору передается 0 в качестве стартовой позиции трансформера (это и есть вышеупомянутая инициализация). Ключевое слово new в некоторых языках отсутствует, и конструктор вызывается автоматически при попытке вызвать класс как функцию, например так: Transformer().

4. Методы constructor и run работают с внутренним состоянием, а во всем остальном не отличаются от обычных функций. Даже синтаксис объявления совпадает.

5. Классы могут обладать методами, которым не нужно состояние и, как следствие, создание объекта. В этом случае метод делают статическим.

SRP


(Single Responsibility Principle / Принцип единственной ответственности / Первый принцип SOLID). С ним вы, наверняка, уже знакомы из других парадигм: «одна функция должна выполнять только одно законченное действие». Этот принцип справедлив и для классов: «Один класс должен отвечать за какую-то одну задачу». К сожалению с классами сложнее определить грань, которую нужно пересечь, чтобы принцип нарушался.

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

Ассоциация


Традиционно в полях объекта могут храниться не только обычные переменные стандартных типов, но и другие объекты. А эти объекты могут в свою очередь хранить какие-то другие объекты и так далее, образуя дерево (иногда граф) объектов. Это отношение называется ассоциацией.

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

class Gun(){ // объявляем класс Пушка
    int ammo_count // объявляем количество боеприпасов

    function constructor(){ // конструктор
        this.reload() // вызываем собственный метод "перезарядить"
    }

    function fire(){ // объявляем метод пушки "стрелять"
        this.ammo_count -= 1 // расходуем боеприпас из собственного магазина
    }

    function reload(){ // объявляем метод "перезарядить"
        this.ammo_count = 10 // забиваем собственный магазин боеприпасами
    }
}

class Transformer(){ // объявляем класс Трансформер
    Gun gun_left // объявляем поле "левая пушка" типа Пушка
    Gun gun_right // объявляем поле "правая пушка" тоже типа Пушка
    
    /*
    теперь конструктор Трансформера принимает
    в качестве аргументов две уже конкретные созданные пушки,
    которые передаются извне
    */
    function constructor(Gun gun_left, Gun gun_right){
        this.gun_left = gun_left // устанавливаем левую пушку на борт
        this.gun_right = gun_right // устанавливаем правую пушку на борт
    }
    
    // объявляем метод Трансформер "стрелять", который сначала стреляет...
    function fire(){
        // левой пушкой, вызывая ее метод "стрелять"
        this.gun_left.fire()
        // а затем правой пушкой, вызывая такой же метод "стрелять"
        this.gun_right.fire()
    }
}

gun1 = Gun() // создаем первую пушку
gun2 = Gun() // создаем вторую пушку
optimus = Transformer(gun1, gun2) // создаем трансформера, передавая ему обе пушки


Анимация №2

this.gun_left.fire() и this.gun_right.fire() — это обращения к дочерним объектам, которые происходят так же через точки. По первой точке мы обращаемся к атрибуту себя (this.gun_right), получая объект пушки, а по второй точке обращаемся к методу объекта пушки (this.gun_right.fire()).

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

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

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

Ортодоксальная ООП-церковь проповедует нам фундаментальную троицу — инкапсуляцию, полиморфизм и наследование, на которых зиждется весь объектно-ориентированный подход. Разберем их по порядку.



Наследование


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

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

Оптимус Прайм и Мегатрон — оба трансформеры, но один является автоботом, а второй десептиконом. Допустим, что различия между автоботами и десептиконами будут заключаться только в том, что автоботы трансформируются в автомобили, а десептиконы — в авиацию. Все остальные свойства и поведение не будут иметь никакой разницы. В таком случае можно спроектировать систему наследования так: общие черты (бег, стрельба) будут описаны в базовом классе «Трансформер», а различия (трансформация) в двух дочерних классах «Автобот» и «Десептикон».

class Transformer(){ // базовый класс
    function run(){
        // код, отвечающий за бег
    }
    function fire(){
        // код, отвечающий за стрельбу
    }
}

class Autobot(Transformer){ // дочерний класс, наследование от Transformer
    function transform(){
        // код, отвечающий за трансформацию в автомобиль
    }
}

class Decepticon(Transformer){ // дочерний класс, наследование от Transformer
    function transform(){
        // код, отвечающий за трансформацию в самолет
    }
}

optimus = Autobot()
megatron = Decepticon()



Анимация №3

Сей пример наглядно иллюстрирует, как наследование становится одним из способов дедуплицировать код (DRY-принцип) с помощью родительского класса, и одновременно предоставляет возможности для мутации в классах-потомках.

Перегрузка


Если же в классе-потомке переопределить уже существующий метод в классе-родителе, то сработает перегрузка. Это позволяет не дополнять поведение родительского класса, а модифицировать. В момент вызова метода или обращения к полю объекта, поиск атрибута происходит от потомка к самому корню — родителю. То есть, если у автобота вызвать метод fire(), сначала поиск метода производится в классе-потомке — Autobot, а поскольку его там нет, поиск поднимается на ступень выше — в класс Transformer, где и будет обнаружен и вызван.

Неуместное применение


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

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

Применительно к нашей ситуации это будет звучать так:

  1. Автобот является Трансформером? Да, значит выбираем наследование.
  2. Пушка является частью Трансформера? Да, значит — композиция.

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

Наследование статично


Еще одно важное отличие наследования от композиции в том, что наследование имеет статическую природу и устанавливает отношения классов только на этапе интерпретации/компиляции. Композиция же, как мы видели в примерах, позволяет менять отношение сущностей на лету прямо в рантайме — иногда это очень важно, поэтому об этом нужно помнить при выборе отношений (если конечно нет желания использовать метапрограммирование).

Множественное наследование


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



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

Абстрактные классы


Кроме обычных классов в некоторых языках существуют абстрактные классы. От обычных классов они отличаются тем, что нельзя создать объект такого класса. Зачем же нужен такой класс, спросит читатель? Он нужен для того, чтобы от него могли наследоваться потомки — обычные классы, объекты которых уже можно создавать.

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

Полиморфизм


Полиморфизм — свойство системы, позволяющее иметь множество реализаций одного интерфейса. Ничего непонятно. Обратимся к трансформерам.

Положим, у нас есть три трансформера: Оптимус, Мегатрон и Олег. Трансформеры боевые, стало быть обладают методом attack(). Игрок, нажимая у себя на джойстике кнопку «воевать», сообщает игре, чтобы та вызвала метод attack() у трансформера, за которого играет игрок. Но поскольку трансформеры разные, а игра интересная, каждый из них будет атаковать каким-то своим способом. Скажем, Оптимус — объект класса Автобот, а Автоботы снабжаются пушками с плутониевыми боеголовками (да не прогневаются фанаты трансформеров). Мегатрон — Десептикон, и стреляет из плазменной пушки. Олег — басист, и он обзывается. А в чем польза?

Польза полиморфизма в данном примере заключается в том, что код игры ничего не знает о реализации его просьбы, кто как должен атаковать, его задача просто вызвать метод attack(), сигнатура которого одинакова для всех классов персонажей. Это позволяет добавлять новые классы персонажей, или менять методы существующих, не меняя код игры. Это удобно.

Инкапсуляция


Инкапсуляция — это контроль доступа к полям и методам объекта. Под контролем доступа подразумевается не только можно/неможно, но и различные валидации, подгрузки, вычисления и прочее динамическое поведение.

Во многих языках частью инкапсуляции является сокрытие данных. Для этого существуют модификаторы доступа (опишем те, которые есть почти во всех ООП языках):

  • publiс — к атрибуту может получить доступ любой желающий
  • private — к атрибуту могут обращаться только методы данного класса
  • protected — то же, что и private, только доступ получают и наследники класса в том числе

class Transformer(){
    public function constructor(){ }

    protected function setup(){ }

    private function dance(){ }
}

Как правильно выбрать модификатор доступа? В простейшем случае так: если метод должен быть доступен внешнему коду, выбираем public. В противном случае — private. Если есть наследование, то может потребоваться protected в случае, когда метод не должен вызываться снаружи, но должен вызываться потомками.

Аксессоры (геттеры и сеттеры)


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

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

Интерфейсы


Задача интерфейса — снизить уровень зависимости сущностей друг от друга, добавив больше абстракции.

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

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

Классы с интерфейсами состоят в отношении «многие ко многим»: один класс может имплементировать множество интерфейсов, и каждый интерфейс, в свою очередь, может имплементироваться многими классами.

У интерфейса двустороннее применение:

  1. По одну сторону интерфейса — классы, имплементирующие данный интерфейс.
  2. По другую сторону — потребители, которые используют этот интерфейс в качестве описания типа данных, с которым они (потребители) работают.

Например, если какой-то объект помимо основного поведения, может быть сериализован, то пускай он имплементирует интерфейс «Сериализуемый». А если объект можно склонировать, то пусть он имплементирует еще один интерфейс — «Клонируемый». И если у нас есть какой-то транспортный модуль, который передает объекты по сети, он будет принимать любые объекты, имплементирующие интерфейс «Сериализуемый».

Представим, что каркас трансформера оборудован тремя слотами: слот для оружия, для генератора энергии и для какого-нибудь сканера. Эти слоты обладают определенными интерфейсами: в каждый слот можно установить только подходящее оборудование. В слот для оружия можно установить ракетную установку или лазерную пушку, в слот для генератора энергии — ядерный реактор или РИТЭГ (радиоизотопный термоэлектрический генератор), а в слот для сканера — радар или лидар. Суть в том, что каждый слот имеет универсальный интерфейс подключения, а уже конкретные устройства должны соответствовать этому интерфейсу. К примеру, на материнских платах используется несколько типов слотов: слот для процессора позволяет подключать различные процессоры, подходящие под данный сокет, а слот SATA — любой SSD или HDD накопитель или даже CD/DVD.

Обращаю внимание, что получившаяся система слотов у трансформеров — это пример использования композиции. Если же оборудование в слотах будет сменным в ходе жизни трансформера, то тогда это уже агрегация. Для наглядности, мы будем называть интерфейсы, как принято в некоторых языках, добавляя заглавную «И» перед именем: IWeapon, IEnergyGenerator, IScanner.

// описания интерфейсов:

interface IWeapon{
    function fire() {} // декларация метода без имплементации. Ниже аналогично
}

interface IEnergyGenerator{
    // тут уже два метода, которые должны будут реализовать классы:
    function generate_energy() {} // первый
    function load_fuel() {}       // второй
}

interface IScanner{
    function scan() {}
}


// классы, реализующие интерфейсы:

class RocketLauncher() : IWeapon
{
    function fire(){
        // имплементация запуска ракеты
    }
}

class LaserGun() : IWeapon
{
    function fire(){
        // имплементация выстрела лазером
    }
}

class NuclearReactor() : IEnergyGenerator
{
    function generate_energy(){
        // имплементация генерации энергии ядерным реактором
    }
	
    function load_fuel(){
        // имплементация загрузки урановых стержней
    }
}

class RITEG() : IEnergyGenerator
{
    function generate_energy(){
        // имплементация генерации энергии РИТЭГ
    }
	
    function load_fuel(){
        // имплементация загрузки РИТЭГ-пеллет
    }
}

class Radar() : IScanner
{
    function scan(){
        // имплементация использования радиолокации
    }	
}

class Lidar() : IScanner
{
    function scan(){
        // имплементация использования оптической локации
    }
}

// класс - потребитель:

class Transformer() {
    // привет, композиция:
    IWeapon slot_weapon   // Интерфейсы указаны в качестве типов данных.
    IEnergyGenerator slot_energy_generator // Они могут принимать любые объекты,
    IScanner slot_scanner // которые имплементируют указанный интерфейс
	
    /*
    в параметрах методов интерфейс тоже указан как тип данных,
    метод может принимать объект любого класса,
    имплементирующий данный интерфейс:
    */
    function install_weapon(IWeapon weapon){ 
        this.slot_weapon = weapon
    }
	
    function install_energy_generator(IEnergyGenerator energy_generator){
        this.slot_energy_generator = energy_generator
    }
	
    function install_scanner(IScanner scanner){
        this.slot_scanner = scanner
    }
}

// фабрика трансформеров

class TransformerFactory(){
    function build_some_transformer() {
       	transformer = new Transformer()
       	laser_gun = new LaserGun()
       	nuclear_reactor = new NuclearReactor()
       	radar = new Radar()
       	
       	transformer.install_weapon(laser_gun)
       	transformer.install_energy_generator(nuclear_reactor)
       	transformer.install_scanner(radar)
        	
        return transformer
    }
}

// использование

transformer_factory = new TransformerFactory()
oleg = transformer_factory.build_some_transformer()



Анимация №4

К сожалению, в картинку не влезла фабрика, но она все равно необязательна, трансформера можно собрать и во дворе.

Обозначенный на картинке слой абстракции в виде интерфейсов между слоем имплементации и слоем-потребителем дает возможность абстрагировать одних от других. Вы можете это наблюдать, посмотрев на каждый слой в отдельности: в слое имплементации (слева) нет ни слова про класс Transformer, а в слое-потребителе (справа) нет ни слова про конкретные имплементации (там нет слов Radar, RocketLauncher, NuclearReactor и т. д.)

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

Утиная типизация


Явление, которое мы наблюдаем в получившейся архитектуре, называется утиной типизацией: если что-то крякает как утка, плавает как утка, и выглядит как утка, то, скорее всего — это утка.

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

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



ISP

(Interface Segregation Principle / Принцип разделения интерфейса / Четвертый принцип SOLID) призывает не создавать жирные универсальные интерфейсы. Вместо этого интерфейсы нужно разделять на более мелкие и специализированные, это поможет гибче их комбинировать в имплементирующих классах, не заставляя имплементировать лишние методы.

Абстракция


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

Уровень абстракции


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

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

Неверный выбор уровня абстракции ведет к одной из двух проблем:

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



Еще важно понимать, что уровень абстракции определяется не для всего проекта в целом, а отдельно для разных компонентов. В каких-то местах системы абстракции может быть недостаточно, а где-то наоборот — перебор. Однако, неверный выбор уровня абстракции можно исправить своевременным рефакторингом. Ключевое слово — своевременным. Запоздалый рефакторинг провести проблематично, когда на данном уровне абстракции реализовано уже множество механизмов. Проводить обряд рефакторинга в запущенных системах может сопрягаться с острой болью в труднодоступных местах программиста. Это примерно как поменять фундамент в доме — дешевле построить рядом дом с нуля.

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

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

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

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

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



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

Паттерны проектирования




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

Паттерны проектирования, как и абстракция, свойственны не только ООП разработке, но и другим парадигмам. Вообще, тема паттернов выходит за рамки данной статьи, но здесь хотелось бы предостеречь молодого разработчика, который только намерен познакомиться с паттернами. Это ловушка! Сейчас объясню, почему.

Предназначение паттернов — помощь в решении архитектурных проблем, которые либо уже обнаружились, либо вероятнее всего обнаружатся в ходе развития проекта. Так вот, прочитав про паттерны, у новичка может появится непреодолимый соблазн использовать паттерны не для решения проблем, а для их порождения. А поскольку разработчик в своих желаниях необуздан, он может начать не решать задачу при помощи паттернов, а подстраивать любые задачи под решения с помощью паттернов.

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

Заключение


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

Хочется закончить свою песнь важными словами. Любые описанные механизмы, принципы и паттерны, как и ООП в целом не стоит применять там, где это бессмысленно или может навредить. Это ведет к появлению статей со странными заголовками типа «Наследование — причина преждевременного старения» или «Синглтон может приводить к онкологическим заболеваниям».

Я серьезно. Если рассмотреть случай с синглтоном, то его повсеместное применение без знания дела, стало причиной серьезных архитектурных проблем во многих проектах. И любители забивать гвозди микроскопом любезно его нарекли антипаттерном. Будьте благоразумны.

К сожалению, в проектировании не существует однозначных рецептов на все случаи жизни, где что применять уместно, а где неуместно. Это будет постепенно укладываться в голове с опытом.

Комментарии (79)


  1. Kanut
    16.08.2019 08:33

    Неплохо. И на мой взгляд полезно к прочтение для начинающих. Как минимум полезнее чем вот такие вещи: «Заблуждения начинающих C# разработчиков. Пытаемся ответить на стандартные вопросы»


  1. Gennadii_M
    16.08.2019 09:36
    +1

    Читанул по-диагонали — вроде всё очень даже гут. Спасибо за материал, солилная работа! Буду использовать как референс для интересующихся людей )


  1. morsic
    16.08.2019 09:51
    +1

    Вы описали не инкапсуляцию, а сокрытие.


    1. AlexBin Автор
      16.08.2019 10:07

      Вы описали не инкапсуляцию, а сокрытие.

      Я описал и инкапсуляцию, и сокрытие, но мне не стоило их смешивать. Спасибо. Вечером поправлю.


  1. Veidt
    16.08.2019 10:06
    +1

    Весьма толковая статья про ООП (и не только, учитывая что как правильно заметил автор абстракция и паттерны не имеют к нему прямого отношения).

    Единственное что, возможно выскажу непопулярное мнение, но:

    • Интерфейсы — это такой костыль для языков, разработчики которых не хотели реализовывать множественное наследование, а точнее бороться с «ромбами» реализаций. Классическая иллюстрация — Java, где после появления default реализаций, интерфейсы это по сути классы без non-static field'ов. То есть абстракция получилась очень дырявая.
    • Паттерны проектирования, в общем случае — зло. Либо вынужденное зло — когда используются как костыль, чтобы скрыть недостатки используемых языков / платформ (например паттерн Model-View), либо, что еще хуже — лень, когда разработчик не пытается использовать медленное мышление — структурировать задачу и придумать правильное решение, а включает быстрое мышление и начинает мыслить шаблонами, городя «фабрики фабрик» со всеми вытекающими.

    Ну и к инкапсуляции, как к коктейлю, который одновременно отвечает за обеспечение модульности / пространств имен, реализацию полиморфизма (причем одиночного), синтаксический сахар в виде автоподстановки первого параметра (this), ну и функционал структур (то есть реализацию работы с первичными данными — field'ами) тоже есть много вопросов. В частности, например, она усугубляет объектно-реляционный семантический разрыв. Но это отдельная большая тема.


    1. bm13kk
      16.08.2019 11:26
      +2

      Интерфейсы и вообще система тимов — это метаинформация для компилятора которая дает ему больше информации, как можно еще лучше оптимизировать код. А ИДЕ возможность найти ошибку еще до компиляции. Зло — это когда машинный код после компиляции все еще имеет в себе куски типизации и тратит время и стек на проверку типов в рантайме.

      Паттерны проектирования — это процесс стандартизации индустрии. Как минимум чтобы люди друг друга понимали. Как максимум — чтобы была возможность переиспользовать код. Зло — это когда почти каждая программа имеет свой текстовый редактор и спеллчекер, когда нам нужна 20Гб операционка чтобы отредактировать *.txt файл.


      1. Veidt
        16.08.2019 12:25

        Зло — это когда машинный код после компиляции все еще имеет в себе куски типизации и тратит время и стек на проверку типов в рантайме.

        Насколько я знаю, мало какой машинный код проверяет типы просто ради проверки (чтобы выкинуть ошибку). Обычно машинный код использует информацию построенную на основе типов, например, таблицы виртуализации, но альтернатива им в виде условных переходов не сильно оптимальнее.

        Ну и не понятно какое отношение это имеет к «дырявости» интерфейсов (в смысле их дублированию логики классов).

        Паттерны проектирования — это процесс стандартизации индустрии. Как минимум чтобы люди друг друга понимали. Как максимум — чтобы была возможность переиспользовать код. Зло — это когда почти каждая программа имеет свой текстовый редактор и спеллчекер, когда нам нужна 20Гб операционка чтобы отредактировать *.txt файл.

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


        1. Kanut
          16.08.2019 12:33

          Если честно я тоже не совсем понимаю что вы так прямо чуть ли не ненавидите паттерны. Возьмём те же IOC или Dependency Injection: вы считаете что они не нужны, что они «зло», «костыль», «неправильная проектировка архитектуры» или «нежеленаие разработчиков найти правильное решение»?


          1. Veidt
            16.08.2019 12:45

            Если честно я тоже не совсем понимаю что вы так прямо чуть ли не ненавидите паттерны. Возьмём те же IOC или Dependency Injection: вы считаете что они не нужны, что они «зло», «костыль», «неправильная проектировка архитектуры» или «нежеленаие разработчиков найти правильное решение»?


            А причем тут IOC и DI. Они не создают дырявые абстракции (а точнее классы/объекты), и достаточно спрятаны от разработчика (в коде всего одна аннотация).

            Я про всякие фабрики, хранители, посредники и другие суррогаты по ссылке в статье.


            1. Kanut
              16.08.2019 12:55
              +2

              А причем тут IOC и DI.

              При том что они на мой взгляд вполне себе являются «паттернами проектирования», а вы выше написали:
              Паттерны проектирования, в общем случае — зло


              И даже если не относить IoC и DI к паттернам, с чем многие вполне себе могут поспорить, то IoC и DI тоже можно использовать не к месту и/или криво и тем самым насоздавать себе кучу проблем. Они в этом плане мало чем отличаются от фабрик, декораторов, фасад и прочего.

              Практически любой паттерн имеет свою область применения и вредит если используется не к месту. Но это не значит что паттерны зло «в общем случае»…


              1. Veidt
                16.08.2019 14:15

                При том что они на мой взгляд вполне себе являются «паттернами проектирования»

                Я специально зашел по ссылке автора и там ни IoC ни DI нет. А я под паттернами проектирования подразумеваю достаточно конкретную вещь (помню даже книжку по ним лет 15 назад читал) и именно то, что описано по ссылке автора.

                И, еще раз, эти концепции (IoC и DI) не создают дополнительные публичные классы и объекты. Это принципиальный момент, поэтому они практически не вносят дополнительной сложности.


                1. Kanut
                  16.08.2019 14:34
                  +1

                  Я специально зашел по ссылке автора и там ни IoC ни DI нет.

                  Если я не ошибаюсь, то внизу там была ссылка на DI, да и так нелюбимая вами «фабрика» это вроде бы один из вариантов IoC.
                  Кроме того как минимум в английской версии статьи они присутствуют оба: en.wikipedia.org/wiki/Software_design_pattern

                  А я под паттернами проектирования подразумеваю достаточно конкретную вещь (помню даже книжку по ним лет 15 назад читал) и именно то, что описано по ссылке автора.

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

                  И, еще раз, эти концепции (IoC и DI) не создают дополнительные публичные классы и объекты.


                  Во первых они создают оверхед, как минимум в виде конфигурации и мэппинга. Во вторых они часто создают проблемы с зависимостями от библиотек или их отсуствием. И в третьих я конечно не знаю что вы сейчас конкретно подразумеваете под «дополнительные публичные классы и объекты», но практически все DI варианты на C#, которые я видел, имеют какой-нибудь DependencyLocator и/или коллекцию мэппингов.


                  1. Veidt
                    16.08.2019 15:30

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

                    Просто если абстрагировать паттерны проектирования до всех «техник применяемых в программировании», то они автоматически потеряют всякий смысл, так как будут значить все что угодно. Так и само ООП можно паттерном проектирования считать.
                    Если я не ошибаюсь, то внизу там была ссылка на DI, да и так нелюбимая вами «фабрика» это вроде бы один из вариантов IoC.

                    Давайте так, чтобы уточнить, когда я говорил про IoC и DI я имел ввиду, что они встроены в язык / платформу (вроде как в Java Spring / .Net). И все создаваемые при этом классы разработчику не видны.

                    А когда IoC и DI делают явно в коде (создавая proxy-классы и заменяя на них field'ы и конструкторы) это ад. Такой код читать / рефакторить очень тяжело.


                    1. Kanut
                      16.08.2019 15:41

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

                      Угу. Вот только почему-то большинство известных мне программистов считаю DI и IoC паттернами. Да и гугл с вики с этим вроде бы согласны :)

                      Давайте так, чтобы уточнить, когда я говорил про IoC и DI я имел ввиду, что они встроены в язык / платформу (вроде как в Java Spring / .Net). И все создаваемые при этом классы разработчику не видны.

                      Более менее «встроены» они начиная с .Net Core и даже там они невидимыми не являются, так как минимум конфигурировать всё это дело кому-то надо.

                      А когда IoC и DI делают явно в коде (создавая proxy-классы и заменяя на них field'ы и конструкторы) это ад. Такой код читать / рефакторить очень тяжело

                      Угу. И именно так всё это делалось пока не появились удобные фреймворки. И даже такой вариант на мой взгляд раньше часто являлся лучшим решением чем не иметь никакого IoC :)


                      1. Veidt
                        16.08.2019 15:57

                        Угу. И именно так всё это делалось пока не появились удобные фреймворки. И даже такой вариант на мой взгляд раньше часто являлся лучшим решением чем не иметь никакого IoC :)

                        До фреймворков они были вынужденным злом:
                        когда используются как костыль, чтобы скрыть недостатки используемых языков / платформ

                        После фреймворков, они стали частью языка / платформы и перестали быть паттернами, в моем понимании этого слова.

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


                        1. Kanut
                          16.08.2019 16:10

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

                          И честно говоря пока не в одном определении паттерна, которое я встречал в своей жизни, не видел чтобы паттерн описывали как «классы которые создает разработчик руками и они видны в коде, попадают во все поиски использований, графы вызовов и т.п.» :)


            1. michael_vostrikov
              16.08.2019 21:01
              +1

              Я про всякие фабрики, хранители, посредники и другие суррогаты по ссылке в статье.

              И почему вы считаете, что они суррогаты?


        1. bm13kk
          16.08.2019 14:48
          +1

          > Насколько я знаю, мало какой машинный код проверяет типы просто ради проверки

          да, но…

          > Обычно… таблицы виртуализации, но альтернатива…

          А почему компилятор не смог в байт код без таблицы? Без альтернатив? У него есть последовательность данных и последовательность операций которые сразу синхронизированны. Как часто мы действительно не можем обойти данные потому что не знаем точно что внутри? Почему в таких случаях скомпилированная микро* машина состояний не лучше?

          *одна функция до сотни инструкций.

          Мое личное мнение — потому что компилятор не смог достаточно точно угадать что там будет. Это происходит по двум причинам. Первая — программисты ленивы и пишут «минимально рабочий» код. Который и стабильный и читабельный. Но работает со слишком большим спектром входящих данных. Вторая — мы не пишем языки (за редким исключением) которые помогают передать компилятору бОльше информации. Которые помогают компилятору увидеть что входящий поток данных имеет низкую вариативность и может быть существенно упрощен.

          > Ну и не понятно какое отношение это имеет к «дырявости» интерфейсов (в смысле их дублированию логики классов).

          Потому что интерфейс — просто разновидность системы типов. То что отдельно взятые языки пошли по пути наименьшего сопротивления и понапихали синтаксических костылей. Я согласен что «дефолтная реализация» интерфейсов портит целостность языка. В то же время — я хорошо понимаю почему это добавили и как облегчили жизнь разработчикам.

          Однако Ваш нонконформизм слишком мелочен. Зачем недолюбливать интерфейсы? Идите глубже — нелюбите само наследование. Наследование — это такое же зло как и нулл. От него тоже надо отказаться.

          Это не ирония. Я действительно считаю, что от наследования нужно избавляться. Однако отказаться одновренменно и от наследования и от интерфейсов — настоящий вызов. Возможно даже здравому смыслу, потому что без наследования я не вижу никаких недостатков у наследования. Одни преимущества.

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

          Сказка для детей. Я не отрицаю что в нее верят и некоторые студенты, однако они просто еще не доросли. Понятного кода не существует. Если Вы считаете что Ваш код понятен — Вы его просто не показали достаточному числу людей.

          Код может быть интересным, красивым, оригинальным и прочее. Но не понятным. Обьяснить код машине проще, чем человеку. И подавляющее время программирование — поиск и исправление ошибочного понимания кода машиной. Хотя не правда, все наоборот. Машина всегда понимает код правильно. За оооочень редким исплючением. Это мы не правильно ей обьяснили с первых N раз.

          > Какое отношение паттерны проектирования имеют к повторному использованию кода (в частности к переписыванию текстовых редакторов) тоже неясно…

          Не паттерны как таковые (хотя тут есть что обсудить) а сам процесс стандартизации.

          А стандартизация невозможна без паттернов. Если мы не видем что одно и тоже сделано на разных языках, если мы не имеем общего термина для этого — двигаться дальше невозможно.

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

          PS Код может понятен тысяче программистов только при условии существования паттернов. Только потому что им не надо читать и компилировать каждую строку у себя в голове. А можно увидеть знакомые куски и пропустить минимум половину кода.


        1. michael_vostrikov
          16.08.2019 20:08

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

          И понятным он становится в том числе и из-за паттернов. А когда человек изобретает те же паттерны, только называет их по своему, код становится непонятным.


    1. mayorovp
      16.08.2019 13:11

      Интерфейсы — это такой костыль для языков, разработчики которых не хотели реализовывать множественное наследование, а точнее бороться с «ромбами» реализаций. Классическая иллюстрация — Java, где после появления default реализаций, интерфейсы это по сути классы без non-static field'ов. То есть абстракция получилась очень дырявая.

      С другой стороны, проблема "ромба" именно при наличии полей в базовом классе и проявляется наиболее сильно!


    1. AlexBin Автор
      16.08.2019 13:34

      Интерфейсы — это такой костыль для языков

      Про интерфейсы хотелось бы услышать мнение господина lair


      1. lair
        16.08.2019 13:39

        Что конкретно?


        1. AlexBin Автор
          16.08.2019 13:44

          Что конкретно?

          Что думаете по поводу этой мысли?


          1. lair
            16.08.2019 16:39

            Думаю, что там сильно больше одной мысли. Но если по верхам:


            Интерфейсы — это такой костыль для языков, разработчики которых не хотели реализовывать множественное наследование

            Мне не нравится слово "костыль", но по сути это недалеко от того, что я думаю: для потребителя (то есть того, кто откуда-то получает Something some, а потом делает ему some.Do()) различие между интерфейсом и классом сугубо техническое, и без него прекрасно можно обойтись. При этом разделение на реализацию и абстракцию (или, точнее, внешний контракт) — очень важно; не важно, чем этот контракт выражается, важно, чтобы были способы его знать и, по возможности, навязывать.


            Паттерны проектирования, в общем случае — зло.

            … а по этому поводу уже все сказали: в общем случае, паттерны проектирования — это совсем не зло, потому что это типовые решения типовых задач. Общий словарь.


    1. StrangerInTheKy
      16.08.2019 16:20
      +1

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


      1. BD9
        17.08.2019 19:15

        более привычное русскому уху слово «шаблон»

        Перевод слов «паттерн» и «шаблон» — образец.
        Есть такое понятие — «стандартный образец».


  1. White_Scorpion
    16.08.2019 12:56

    Почему бы для рисования сразу не использовать UML?


    1. AlexBin Автор
      16.08.2019 13:11

      Почему бы для рисования сразу не использовать UML?

      В статье написано, почему. Или я не понял вопрос.


      1. White_Scorpion
        16.08.2019 18:51

        Я не стал использовать UML-диаграммы, посчитав их недостаточно наглядными, хоть и более гибкими.

        Слабое объяснение. Крайне субъективное и слабое. Но мало того: UML — есть стандарт, а ваша методика рисования — есть только ваша методика рисования. Вот возьмёт новичок и по вашим диаграммам, пойдёт в профессию и столкнётся с профессионалом, который не долго думаючи спросит: "а почему ты, новичок, рисуешь диаграммы как-то странно? Ты что — дурак? Ведь есть же UML — зачем ты переизобрёл велосипед в 115 раз?"
        Как будет чувствовать себя новичок?


        1. lair
          16.08.2019 19:02
          +1

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


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


          1. White_Scorpion
            19.08.2019 11:38

            К любому "рисованию диаграмм по своему" шли через БАЗУ рисования диаграм вообще. А база в IT для этого — UML диаграммы.


            Мне когда то, ещё в те времена молодому профессионалу, вложили в голову, что без знания базы — разработчики не будут понимать вообще что нужно делать и я с этим мнением — полностью согласен. Потому что именно база приводит идеи к общему знаменателю, понятному всем заинтересованным персонам.


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


            1. lair
              19.08.2019 11:41
              +1

              А база в IT для этого — UML диаграммы.

              … а на чем основано это утверждение?


              Мне когда то, ещё в те времена молодому профессионалу, вложили в голову, что без знания базы — разработчики не будут понимать вообще что нужно делать и я с этим мнением — полностью согласен. Потому что именно база приводит идеи к общему знаменателю, понятному всем заинтересованным персонам.

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


              Особенно если учесть, что UML не имеет строгой нотации

              Вот именно поэтому я и не вижу смысла за него ратовать, если споров за то, как в нем что-то правильно изобразить, будет больше, чем споров об изображенном (real life story).


              1. White_Scorpion
                19.08.2019 14:55

                … а на чем основано это утверждение?

                Первая буква U из UML — отвечает на этот вопрос.


                без отсутствия общего языка. Но вы почему-то считаете, что UML таким языком является

                А чем вам UML не общий? И без шпаргалки его сможет прочесть любой, сноски нужны — для понимания нюансов. Но вот в чём штука — чтобы понять ЛЮБУЮ предлагаемую ВАМИ (или автором) диаграмму — сноски обязательны. Но в вашем случае — они нужны априори, а в UML без них, за счёт хоть какой-то известности — можно обойтись. Так что он уже более общий, нежели ваш (или автора) продукт.


                как в нем что-то правильно изобразить

                Кто определит понятие "правильность изображения" — тот собственно и решит все споры.


                1. lair
                  19.08.2019 15:03
                  +1

                  Первая буква U из UML — отвечает на этот вопрос.

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


                  А чем вам UML не общий?

                  Тем, что не все его знают.


                  И без шпаргалки его сможет прочесть любой

                  Неа. И, что важнее, все прочитают по-разному.


                  Но в вашем случае — они нужны априори, а в UML без них, за счёт хоть какой-то известности — можно обойтись.

                  Вот это как раз и иллюзия. Может быть можно. На практике я такого не видел.


                  1. White_Scorpion
                    19.08.2019 16:32

                    как кто-то решил назвать свой язык.

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


                    Неа. И, что важнее, все прочитают по-разному.

                    ну не знаю не знаю, но класс — я различу. Интерфейс тоже… С ассоциациями — морока конечно, но тоже можно разобраться.
                    А с другой стороны — я не знаю как в России, но я кроме двух ВУЗов в Латвии знаю как минимум один в Германии — в которых в IT на программиста преподают UML. И представляют как стандарт. Я знаю, что используется в некоторых ВУЗах США.
                    Это как минимум в несколько раз больше, нежели — изобрести велосипед заново в данной статье.


                    На практике я такого не видел.

                    Субъективное мнение. Я видел, как люди — не пугались UMLа :).


                    1. lair
                      19.08.2019 16:37

                      Не кто-то, а люди, которые в далёком 1995 году уже столкнулись с проблемой рисованяи классов и постарались решить эту проблему

                      Никто не спорит с тем, что постарались. Но решили ли?


                      ну не знаю не знаю, но класс — я различу. Интерфейс тоже…

                      Вы — различите. Но вы — это не все.


                      А с другой стороны — я не знаю как в России, но я кроме двух ВУЗов в Латвии знаю как минимум один в Германии — в которых в IT на программиста преподают UML. И представляют как стандарт.

                      А те, кто в этих вузах не учился, в разработке ПО не работают? Мне вот никто и никогда не преподавал UML, мне это работать не мешает никак в смысле "совсем никак".


                      Это как минимум в несколько раз больше, нежели — изобрести велосипед заново в данной статье.

                      Мне кажется, вы не понимаете целей "велосипеда" в данной статье. У него ровно одна задача: пояснять рассуждения в данной статье. Смысла сравнивать его с преподаванием UML ровно ноль.


                      Субъективное мнение.

                      Ну да, субъективное. Но оно прекрасно показывает общую идею: бесполезно ожидать, что если вы нарисуете диаграмму в UML, ее поймут все и одинаково.


                    1. Kanut
                      19.08.2019 16:40

                      У нас в ВУЗе UML преподавали и именно как общий стандарт.


                      Проблема только в том, что в каждой фирме где я работал был свой "диалект" и каждый раз приходилось привыкать/переучиваться заново.


                    1. AlexBin Автор
                      19.08.2019 18:22

                      изобрести велосипед заново в данной статье

                      Велосипед — это изобретение чего-то уже изобретенного. Я тоже сначала хотел UML, но уперся в то, что не нашел, как изобразить с помощью него некоторые моменты, описанные в данном комментарии

                      Понимаете, это как цифры и числа. Нельзя преподавать человеку числа, если он не знает цифр. UML — это числа, а мои схемы — это цифры. UML помогает описывать архитектуру с помощью известных всем механизмов. А мои картинки описывают, как работают эти самые механизмы. Это разные уровни.


        1. AlexBin Автор
          16.08.2019 19:28

          Крайне субъективное и слабое

          Никто и не говорил, что это объективное обоснование. Там так и написано — это мое субъективное мнение.

          А вот, что объяснение слабое, давайте обсудим. Можете нарисовать с помощью UML так чтобы было понятно, что класс отличается от объекта, что код метода содержится в классе, а состояние в объекте, что в интерфейсе только сигнатура метода, а в классе — имплементация, что метод fire после перегрузки будет вызываться из имплементации класса десептикона, а не трансформера? Если сможете, то вы правы, это объяснение слабое.

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

          Если бы у меня была цель вместо букваря детям дать чертежи, я бы назвал статью «ООП в UML-диаграммах», а не в картинках. И тогда да, дети после первого класса могут сразу идти на собеседование, владея чертежами.


  1. den_labs
    16.08.2019 14:33

    Чтиво не осилил, плюсую за картинки).


    1. onground
      17.08.2019 20:50

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


  1. WebMonet
    16.08.2019 15:17

    С теорией применения паттернов все вроде-бы понятно. Теперь хочется научиться правильно декомпозировать задачи (посоветуйте материалов) и правильно применять паттерны, особенно в рефакторинге легаси-кода.
    Хочется примеров «best practice», признаков того, что все сделано правильно.


    1. Kanut
      16.08.2019 15:26
      +1

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

      И какого-то «best practice» на все случаи жизни лично я не знаю. Я даже не могу назвать «best practice» для выбора «best practice для какой-то конкретной проблемы» :)


      1. AlexBin Автор
        16.08.2019 15:29

        Как раз это я пытался донести в разделе «заключение».


    1. dim2r
      16.08.2019 15:50

      Не все задачи легко декомпозируются в ООП. Попробуйте для разминки декомпозировать геометрию.


    1. michael_vostrikov
      16.08.2019 20:19
      +2

      и правильно применять паттерны, особенно в рефакторинге легаси-кода.

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


  1. makssof
    16.08.2019 17:08

    Немного занудства.
    Заголовок: ООП в картинках.
    А по факту, наиболее подходящая под заголовок картинка лишь предпоследняя (со слоями абстракций), а всё остальное — простые UML-диаграммы.


  1. Daddy_Cool
    17.08.2019 01:20

    Стопицотый раз читаю статью по ООП. Для себя нашел проблему — почему я так и не могу нормально разобраться в ООП (увы). Я привык к стилю программированию на чистом Си (в основном всякой математики), без ++, и соответственно возникает вопрос — а зачем мне использовать ООП, если можно и без него. Но это-то ладно — я могу сообразить, что некие объекты лучше сделать классами — ну скажем сделать если я захочу комплексных чисел или кватернионов, я напишу соответствующий класс и определю операции взаимодействия, но дальше… какие задачи стоит решать именно через ООП? Я с удовольствием бы увидел примеры с объяснениями — типа вот задача, мы её решаем так-то и делаем это именно так по таким-то причинам…


    1. Kanut
      17.08.2019 12:02
      +1

      Хм… Я бы не сказал что существуют задачи, которые обязательно нужно решать при помощи ООП. В конце-концов любой ООП код трансформируется в машинный и следовательно любую проблему «решаемую при помощи ООП» можно решить сразу написав нужный машинный код.

      ООП это, в контексте вашего вопроса, просто «инструмент», который по идее просто облегчает написание и поддержку кода.

      И такое часто просто вопрос привычки и/или необходимости. И если лично вам проще без этого и/или вы без этого прекрасно обходитесь, то возможно ООП вам и не нужно.


    1. AlexBin Автор
      17.08.2019 12:26
      -1

      Я привык к стилю программированию на чистом Си (в основном всякой математики), без ++, и соответственно возникает вопрос — а зачем мне использовать ООП, если можно и без него.

      Действительно, зачем?

      Цитата из статьи:
      Любые описанные механизмы, принципы и паттерны, как и ООП в целом не стоит применять там, где это бессмысленно или может навредить.

      Зачем натягивать сову на глобус, если у вас и без ООП все прекрасно получается. Возможно ООП вам только навредит.


    1. Kwisatz
      17.08.2019 12:49
      +1

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

      class Starship
        {
        private $position;
      
        public function fly(Vector $coords) {}
        public function moveTo($coords) {}
        public function hyperjumpTo($coords) {}
        public function shootAt($coords) {}
        }
      


      $myStarship=new Starship;
      $myStarship->flyTo($someRandomPoint);
      


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

      abstract class Starhip {} 
      
      class StarDestroyer extends Starship {}
      


      Но некоторым нужно подчеркнуть свою индивидуальность

      class SuperStarDestroyer extends Starship {}
      


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

      //Название не совсем удачное, поскольку Imerial/Rebel на самом деле не указывает на владельца
      abstract class ImperialStarship extends Starship {}
      abstract class RebelStarship extends Starship {}
      
      class StarDestroyer extends ImperialStarship {}
      class SuperStarDestroyer extends ImperialStarship {}
      
      class CorelianCorvette extends RebelStarShip {}
      class MonCalamariCruiser extends RebelStarShip {}
      


      Более того мы теперь можем построить верфи

      //ну или Shipyard
      class ImperialWarf () : ImperialStarship {}
      class RebelWarf() : RebelStarship {}
      class CorelianWarf() : Starship {}
      


      Однако мир чуть сложнее, да и зло нам нужно не просто так, а чтобы творить, ну… собственно зло.

      abstract class Starhip 
        {
        public function shootAt(Starship $target)
        }
      


      И наверняка, просто зла недостаточно, нужно вселенское зло

      interface Destroyable {}
      
      abstract class Starship implements Destroyable {}
      abstract class Planet implements Destroyable {}
      
      //ее по канону конечно нужно сделать синглтоном, но кто знает....
      class DeathStar extends Starship
        {
        public function destroy(Destroyable $target)
        }  
      


      Ну и наведем чуточку красоты

      class GalaxyFarFarAway
        {
        private static $planets;
      
        public static function getPlanetByName(String $planetName) {}
        public static function getRandomHabitablePlanet() {}
        }
      


      И как итог, сферический Люк Скайвокер в вакууме, может теперь:

      $myNewShip = new MilleniumFalcon();
      $destination=GalaxyFarFarAway::getPlanetByName('Alderaan')
      
      $myNewShip->hyperjumpTo($destination->getCoords());
      


      А губернатор Таркин, мерзко ухмыляясь:

      $myPrecious = new DeathStar();
      $victim=GalaxyFarFarAway::getRandomHabitablePlanet()
      
      $myPrecious->destroy($victim);
      


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

      При этом: помнить или разбираться сразу во всем необходимости нет вообще никакой. По методам IDE подскажет прямо на ходу. По дереву абстракций ходить тоже легко и просто. Захотели сгенерить документацию? Тоже без проблем.


      1. Daddy_Cool
        17.08.2019 13:43

        «губернатор Таркин, мерзко ухмыляясь»

        ))) Спасибо!!! Хоть прям бери и пишу игрушку по мотивам ЗВ ))).
        Видимо мои программы слишком просты структурно. Хотя возможно я нахожусь в некой психологической ловушке сравнительно низкоуровневого программирования, впрочем высокоуровневое тоже может быть ловушкой — ваял что-то на С++ Builder — ну как ваял — складывал из кубиков, заметил, что мне уже лениво писать какие-то функции самому, лучше я поищу готовые компоненты, а до знакомства с этим чудесным продуктом такого не было.


    1. lair
      17.08.2019 17:39

      какие задачи стоит решать именно через ООП?

      Те, решение которых без ООП оказывается менее поддерживаемым. Если вы пишете математику и на C, скорее всего, вам это низачем не нужно.


    1. juray
      17.08.2019 18:44

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

      Пример — цифровая фильтрация. Пока у нас только один фильтр, мы можем тупо написать функцию типа int filter(int input_value), а всякие там внутренние данные типа массива для фильтрации методом усреднения или хотя бы одного предыдущего значения для КИХ/БИХ спрятать в локальной области видимости модуля. Даже если нужно менять параметры фильтра на ходу — опять же добавляем функцию, присваивающую переданные ей значения внутренним переменным модуля, задающим коэффициенты, или даже флаги, меняющие тип фильтрации.

      По сути, модуль обеспечивает одну из трех фишек ООП — инкапсуляцию. Но у такого модуля есть проблема — он один. Даже если мы размножим наборы хранимых внутренних данных (массив1, массив2, коэффА_1, коэффА_2, коэффБ_1, коэффБ_2), то как указать, с какими из них должна работать функция в каждом случае? Не говоря уже о том, что такое размножение просто неудобно и громоздко.

      Окей, путь есть — упаковываем набор в структуру, используем несколько структур (можно даже динамически создавать экземпляры структур в рантайме по мере надобности) и получаем возможность при вызове функции указывать, с какой из структур работать: filter(value, &dataset1). Но это нарушает инкапсуляцию — чтобы передать в функцию указатель или ссылку на структуру, внешний код должен иметь эту структуру в своей видимости.
      Хотя и это можно обойти — если наборы заданы статично и их немного, можно сразу присвоить им некие идентификаторы, и передавать функциям эти идентификаторы вместо ссылки/указателя. При динамическом же созданий наборов код еще усложняется — надо будет генерировать идентификаторы, где-то хранить соответствие идентификаторов и адресов структур в памяти и т.п. При том что мы по сути уже «изобрели объект».

      И вот тут стоит только перейти от C к С++ и использовать волшебное слово class — и весь этот геморрой оказывается переложен на плечи компилятора, указатель на набор данных объект хранит скрыто внутри себя и снаружи опять виден простой «черный ящик».

      Наследование и полиморфизм тоже можно подобными путями реализовать на чистом С. Это будет еще более громоздко, и что самое главное — это таки будет объектно-ориентированным программированием, только без использования ОО-возможностей компилятора.

      Ну ок, наследование и полиморфизм мне лично в разработке прошивок для микроконтроллеров пока еще не пригождались (только для софта на ПК). А вот инкапсуляция — еще как. Кстати к дихотомии низкоуровневое/высокоуровневое — цифровая фильтрация или там программное подавление дребезга контактов это ведь как раз очень низкоуровневые задачи.


      1. Daddy_Cool
        18.08.2019 21:50

        Здорово, спасибо!!!


    1. juray
      17.08.2019 19:04

      Пожалуй, таки опишу свой пример использования полиморфизма.

      Вот, к примеру, одна моя софтина принимает от железки кучу разных типов данных (даты, числа с довольно разнообразным кодированием и т.п.) и отображает их в интерфейсе, выводит в отчеты и т.п.
      Как это делается чисто процедурным путем — для каждого такого значения вызывается функция преобразования в строку — inttostr, datetostr, temperature1tostr, temperature2tostr.
      Тут есть две засады. Во-первых, код тупо загромождается кучей вызовов этих функций
      label1.Caption = inttostr(parameter1);
      edit2.Text = inttostr(parameter2);

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


  1. Kwisatz
    17.08.2019 10:03

    Классная статья, хотя про интерфейсы малость путанно.

    Не хватает раздела: «а зачем?». Ну серьезно, очень многие так увлекаются, что KISS, YAGNI итд для них не существует, есть только сферический ООП в вакууме. Причем с костылями естественно. Соответственно, растет сложность разработки и поддержки. Как раз то, с чем ООП призван бороться.

    Заключение тоже неплохое. Но надо еще сказать, что даже при наличии ооп, многие переходят к классовому программированию. Ну вот холивара ради(ну все равно почти под каждой статьей про ооп такой есть): допустим ваши трансфомеры начали сражаться друг с другом, соответственно нам понадобиться расчитать опыт/здоровье/щиты/прочие штуки, а потом обработать собственно убийство. Тут сразу возникнет вопрос: а где и как? И на этом этапе многие скажут, что нужен класс CombatManager, которому нужно передать трансормеров, вместо optimus->attack(megatron). Вот только про его существование нужно обязательно знать и помнить, ибо IDE в данном случае просто бесполезна.


    1. AlexBin Автор
      17.08.2019 12:34

      Не хватает раздела: «а зачем?». Ну серьезно, очень многие так увлекаются, что KISS, YAGNI итд для них не существует, есть только сферический ООП в вакууме.

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

      Ну вот холивара ради(ну все равно почти под каждой статьей про ооп такой есть): допустим ваши трансфомеры начали сражаться друг с другом, соответственно нам понадобиться расчитать опыт/здоровье/щиты/прочие штуки, а потом обработать собственно убийство. Тут сразу возникнет вопрос: а где и как? И на этом этапе многие скажут, что нужен класс CombatManager, которому нужно передать трансормеров, вместо optimus->attack(megatron).

      Это не относится к ООП, это относится к проектированию. А само ООП — просто один из возможных инструментов реализации вашей архитектуры.


      1. Kwisatz
        17.08.2019 12:54

        там, где в этом появляется необходимость.

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

        Это не относится к ООП, это относится к проектированию. А само ООП — просто один из возможных инструментов реализации вашей архитектуры.

        Ловко вы ушли от ответа. Даже возразить нечего)


        1. AlexBin Автор
          17.08.2019 12:58

          Это очень мало где поясняется.

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


          1. Kwisatz
            17.08.2019 13:06

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


            1. Kanut
              17.08.2019 13:13

              Определить было ли использование ООП «в результате», то есть постфактум, хорошей или плохой идеей относительно просто. Вопрос в том как определить это заранее ;)


              1. Kwisatz
                17.08.2019 13:22

                Да и после и вовремя и заранее хочется знать)


            1. AlexBin Автор
              17.08.2019 14:18

              Почему нет?

              Потому что, чтобы определить, какая парадигма подойдет для данного проекта лучше, нужно реализовать этот проект в разных парадигмах и сравнить стоимость разработки/поддержки в каждой из них. При этом требуются опытные команды разработчиков для каждой парадигмы. Иначе получится, что ООП-шники начнут пробовать реализовывать проект на ФП, у них получится плохо, и они решат, что ФП отстой, ООП — сила. (Я привел две популярные парадигмы, но их на самом деле много, это тоже учитывайте)

              А поскольку никто такие дорогостоящие эксперименты не ставит, нам приходится сравнивать похожие проекты в разных парадигмах, не имея на руках данных о стоимости поддержки/разработки, оценивая разницу на глазок. Это и есть «свой и чужой опыт», на который мы можем опираться, об этом я и сказал выше.


              1. Kwisatz
                17.08.2019 19:34

                Ах если бы каждый кто пишет статью про ООП прилагал к ней свой опыт, было бы просто шикарно


                1. AlexBin Автор
                  17.08.2019 20:24

                  Ах если бы каждый кто пишет статью про ООП прилагал к ней свой опыт, было бы просто шикарно

                  Согласен. Но это уже будет не статья, а книга про конкретный проект.


        1. Kanut
          17.08.2019 13:04

          Ну если навскидку, то есть несколько «критериев», которые по той или иной причине могут указывать на «полезность» ООП:
          — Сложная и/или иерархическая структура «реальных» объектов/предметов с которыми надо работать.
          — Большой объем работы/проекта.
          — Большое количество разработчиков, участвующих в проекте.
          — Большая вероятность частых и/или больших изменений.
          — Отсутствие критериев исключающих существующие ООП языки/рантаймы/фреймворки(например перформанс или доступные ресурсы в момент выполнения ).

          Но опять же это всё всего лишь индиции и не больше.


          1. AlexBin Автор
            17.08.2019 14:33

            Ну если навскидку, то есть несколько «критериев», которые по той или иной причине могут указывать на «полезность» ООП:

            Хочется сразу уточнить. Под полезностью ООП вы имеете в виду пользу наличия/отсутствия ООП или пользу сравнительно с какой-то другой парадигмой? Если второе, то с какой именно?


            1. Kanut
              17.08.2019 15:07
              +1

              Опять же всё в зависимости от конкретной ситуации, поэтому и кавычки :)

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


  1. strannik_k
    17.08.2019 10:52

    По поводу уровня абстракции в играх.
    При использовании первого или второго уровня для написания ключевого функционала длительность разработки средней игры будет стремиться к бесконечности. Ибо при изменение требований, а меняются они постоянно, придется постоянно переписывать классы базового трансформера и его наследников. А в игре со временем появятся не только трансформеры, но и другие объекты, которые могут обладать не хилой частью функционала трансформеров.

    В играх 3-ий и 4-ый уровни абстракции используются. Хотя, может тут лучше выделить еще один уровень, или я сейчас 4-ый уровень опишу, не знаю.
    В общем, вводятся понятия «компонент», «контейнер». Нет никаких классов трансформеров, пушек. Есть компоненты, отвечающие за характеристики, способности, различный функционал. После добавления компонентов в объект-контейнер, получается конкретный трансформер, пушка или радар.
    К тому же, при таком подходе количество интерфейсов уменьшается раз в 100, т.к. их более эффективно заменяют компоненты. Там, где раньше у класса проверялось наличие интерфейса, будет проверяться наличие компонента.

    По поводу 4-го уровня. Не понятно, тут разве не о крафте в играх говорится?)
    Еще, похоже что относящееся к этому уровню встречается в играх при написании ИИ. Например, в деревьях поведений. Там составными частями являются действия, условия.


    1. AlexBin Автор
      17.08.2019 12:52

      Мне кажется, вы неправильно поняли примеры с уровнями абстракции. Эти 4 уровня придуманы для данной статьи для сферической игры в вакууме. Это не какая-то устоявшаяся в индустрии классификация уровней, поэтому рассматривать все возможные уровни и реализации, не имея на руках ТЗ — бессмысленно.

      По поводу 4-го уровня. Не понятно, тут разве не о крафте в играх говорится?)

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

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

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

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


      1. BD9
        17.08.2019 19:08

        Minecraft?


        1. juray
          17.08.2019 19:15

          А там рецепты разве не прописаны жестко? Получающиеся продукты так уж точно прописаны, ничего нового не сделаешь.


        1. AlexBin Автор
          17.08.2019 20:22

          Minecraft?

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


      1. juray
        17.08.2019 19:14

        Функционал продукта при этом полностью формируется из свойств деталек и их комбинаций.

        Таким образом генерируется оружие в серии игр Borderlands. Правда, игроку этот механизм внутри игры недоступен, только в виде чита — редактора сейвов.
        При этом слова в названии тоже являются «детальками» и влияют на свойства. Есть вопрос совместимости деталек.
        Вот здесь можно поперебирать варианты blmodding.wikidot.com/gear-calculator


      1. strannik_k
        18.08.2019 08:44

        Ага, не правильно понял. Спасибо за разъяснение!


  1. Dzhuz
    17.08.2019 21:43
    +1

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


  1. northmule
    20.08.2019 09:42

    Отличное изложение материала! Однозначно полезно для новичков и профессионалов!


  1. lnache
    20.08.2019 12:39

    Даже зная все выше изложенное, было очень приятно читать.
    Картинки топ, использование трансформеров топ, прям каеф! Спасибо за статью!