Pull to refresh

Comments 43

Пример с перегрузками не имеет ничего общего с полиморфизмом в контексте ООП

Если у тебя есть более конкретный пример в 2-3 строки (и который будет понятен для новичка) то буду рад его добавить вместо моего

У тебя есть коллекция объектов, например, List<Char>; Char - базовый класс персонажа или лучше интерфейс. В любом случае, имеем некую базовую функциональность. Например, персонаж умеет реагировать на события, имеет характеристики здоровья, силы и т.д. Допустим у нас есть метод think(), который обновляет стратегию поведения и обновляет ее при развитии игровой ситуации. В базовом классе мы можем реализовать базовую версию поведения. Например, искать цель, строить маршрут и идти к ней в притык кратчайшим маршрутом, а затем бить.

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

Теперь имеем ситуацию:

Есть базовый класс Char, в нем есть метод think()

Есть наследник Boss - в нем есть своя реализация метода think()

Что произойдет при вызове char.think()?

1) компилятор определит адрес char@Think метода think() класса Char;

2) компилятор определит адрес this объекта char.

3) компилятор сделает вызов метода по адресу Char@Think и кроме явно определенных аргументов передаст дополнительный аргумент this.

4) вм/процессор выполнят нужный метод

Что произойдет при вызое boss.think()?

Все теже 4 шага, но Boss@Think - это совершенно другой адрес и совершенно другой метод. Соответственно все еще вызовется правильный метод.

А теперь вернемся к работе с коллекциями. У нас есть объект мир, который для каждого объекта вызывает метод think. При этом мир не знает, для какой конкретно объекта вызывается метод think(). В коллекцию List<Char> мы можем положить как базовый класс, так и любого его наследника.

пусть имеем цикл for (Char c: chars) {

c.think()

}

Что произойдет при вызове c.think()?

Компилятор видит, что think() - метод класса Char и соответственно определяет адрес метода БАЗОВОГО класса, независимо от того, какого класса объект лежит в коллекции.

Итак, после обхода коллекции класс "Мир" выполнит базовое поведение для всех объектов, независимо от того, какой на самом деле объект там находился.

Но мы определенно хотели другого!

Мы хотим, чтобы при вызове метода, вызывалась та версия, которая определена в том классе, который на самом деле лежит в коллекции. Да, Boss - это тоже Char. Но при вызове Char.think() мы хотим, чтобы выывался метод из класса Boss.

Вот в этот момент и появляется полиморфизм и виртуальные функции. В Java все методы виртуальны и никак иначе. В C++/C# - метод нужно помечать оператором virtual;

Что происходит теперь? В объекте неявно добавляется таблица виртуальных методов (ТВМ).

Эта таблица содержит адрес реального метода think().

Что происходит теперь при создании объекта Char c = new Boss();?

В созданном объекте Boss заполняется таблица виртуальных методов. Для метода think() указывается адрес Boss@think. Далее в переменную c помещается адрес объекта Boss.

Что происходит теперь при вызове метода c.think?

1) Компилятор не знает на какой конкретно объект указывает ссылка/указатель Char c. Но ему этого и не надо. В ТВМ есть правильные адреса.

2) Компилятор достает правильный адрес из ТВМ и делает вызов метода.

Ура! Мы получили динамичное поведение объектов и при этом никаким образом не заботимся об определении с каким конкретно объектом мы работаем.

Кажется, я рассказал ни фига не просто, зато на уровне, как оно происходит под капотом. Кажется, джуну это знать не обязательно, а вот миддл - уже должен понимать.

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

В контексте ООП полиморфизм - это про то, как код одинаково работает с разными реализациями одного интерфейса / наследниками класса.

interface Transport {
  fun moveTo(location: Location)
}

class Car: Transport {
  override fun moveTo(location: Location) {
    ...
  }
}

class Bicycle: Transport {
  override fun moveTo(location: Location) {
    ...
  }
}

fun moveToStart(transport: Transport) {
  transport.moveTo(Location(0, 0))
}

В этом примере функция moveToStart работает с любыми реализациями Transport, используя полиморфизм.

А то что сейчас в примере в статье - это про параметрический полиморфизм. Да, это тоже полиморфизм, но к ООП он не имеет никакого отношения.

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

fun <T> getFirst(list: List<T>): T {
	return list.get(0)
}

А пример из статьи с перегрузками - это как раз ad-hoc полиморфизм, действительно.

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

Ну, компактная то она компактная, только к ООП отношения не имеет, повторюсь

вероятнее всего ожидал ответ наподобие -
Есть несколько видов полиморфизма

  • Параметрический полиморфизм - дженерики.

  • Полиморфизм подтипов - ООП-полиморфизм, выражаемый в переопределении методов при наследовании. Same dunction - different behavior.

  • Ну и ad-hoc (мнимый полиморфизм) - статический полиморфизм, выражающийся в перегрузке методов.

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

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

ох, к сожалению это ощущение будет преследовать нас до конца). Радует что статья уже дает результаты)

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

На данный момент у меня было 4 собеса на мидла, и везде были вопросы в основном про ООП/солид и использование юнити + какие сторонние ассеты приходилось "трогать".

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

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

да в целом так и есть + более глубокое понимание С# и ооп + грамотный подход к архитектуре проекта

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

Думаю неверно расставлены акценты, объясню по-своему, может будет полезно.

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

Сокрытие:
У вас C#, но в целом может достигаться и без модификаторов типа private. Например это могут быть подчёркивания __такие, или локальный скоуп, откуда переменные не выходят наружу. Например в JS модификатора private до сих пор нет, но сокрытие всегда в нём было и будет. Сейчас для этого выдумали решётку #такую.

Наследование:
Для переиспользования кода при ООП. Переиспользование может достигаться и без наследования, например через вызов обычной внешней функции, или через делегирование, или через "воровство" методов других объектов с подменой this, и много ещё как.
Наследник может не иметь доступа к родительским данным и методам. Переиспользование тогда выходит только в вызове конструктора предка или в прочих неявных механизмах языка.
Наследуются экземпляры, а не классы. Классы остаются отдельными, в частности статичные поля у каждого класса свои.
Наследник не "берёт у родителя", а является родителем. "Берёт" при делегировании.

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

Интерфейсы:
Для обобщённого программирования (полиморфизма) в языках со статической типизацией. Чтобы можно было засунуть различные объекты, не имеющие одинакового предка, в одно место использования. Например если требуется где-то объект с методом work, тогда делаем интерфейс с таким методом. И тогда без разницы какой будет класс.


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


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


В вашем примере IMovable есть поля private, protected и static в интерфейсе. На мой взгляд это глупость, потому что интерфейс может быть только публичным, иначе не имеет смысла. И служит для экземпляров, а не для классов, поэтому static там быть не может. Также const в интерфейсе не может быть, потому что константы не принадлежат экземплярам, и интерфейс не может требовать константу от экземпляра. Разве что это какие-то заморочки конкретного языка, например в C# у интерфейсов есть реализация, что противоречит даже вашим словам "отделить описание от реализации".

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

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

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

В документации микрософт по интерфейсам есть вот такие слова :

 Начиная с C# 8.0, член интерфейса может объявлять тело. Этот называется реализацией по умолчанию. Члены с телом позволяют интерфейсу предоставлять реализацию по умолчанию для классов и структур, которые не предоставляют реализацию с переопределением. Кроме того, начиная с C# 8.0, интерфейс может включать следующее.

Начиная с C# 11 интерфейс может объявлять static abstract элементы для всех типов элементов, кроме полей.

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

Подумал подумал, минус заслуженный :) погорячился.

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

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

Это один из принципов солида - "не изменяй то, что уже работает - добавляй новое"

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

Начиная с C# 8.0, член интерфейса может объявлять тело

Я теперь понял ваш посыл - перечислить в картинке всё что может C#. Да, в нём захотели использовать интерфейс как склад, как неймспейс или класс. Я придрался потому, что это всё - противоположность назначения интерфейсов. Если я вижу private или static в интерфейсе, то прежде всего думаю, что автор кода не понимает зачем нужны интерфейсы, или что он столкнулся с какими-то проблемами и ему пришлось это написать. Интерфейс нужен по месту требования, например для параметра функции. Нельзя требовать на входе функции чтобы входящий объект имел приватное поле или чтобы его класс имел статичный метод.

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

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

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

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

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

то вопросов нет, интерфейсы тогда могут помочь.

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

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

Вы несколько не правильно понимаете принцип. SOLID - Это 5 принципов, а не 1. Буква S не даст вам бесконечно разрастаться. Принцип единой ответственности. А буква O - гарантирует вам, что семантика методов не поменяется. SOLID не запрещает править код. Вы можете его править, например, нашли уязвимость - поправили. Косяк нашли - поправили. Уточнили поведение в неясных случаях. Но, если вы нашли такой косяк, например, безопасности, то добро пожаловать - помечаете старый метод Deprecated и создаете новый, безопасный в том же интерфейсе.

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

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

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

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

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

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

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

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

Методы по умолчанию должны быть у предка, а не у интерфейса. Например у предка, реализующего интерфейс. Но C# не поддерживает множественное наследование, что для игр недостаток, так как например ружьё со штыком - это одновременно и ружьё и штык. Одной оси наследования мало, а агрегирование спасает не полностью, оно менее удобно чем наследование и заставляет делать прокси-свойства. В c++ множественное наследование есть. Почему в C# его не сделали, а теперь возвращают в интерфейсах и делают из них класс, не понятно. Ну что есть то есть, вы от этого довольны, а я бурчу вот, у меня идеалы другие - множественное наследование и интерфейсы-не-классы.

Методы по умолчанию должны быть у предка, а не у интерфейса.

Множественное наследование машет вам руками )

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

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

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

Почему в C# его не сделали, а теперь возвращают в интерфейсах и делают из них класс, не понятно.

Да как раз понятно. Потому что проблем от них больше профита.

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

Ну что есть то есть, вы от этого довольны, а я бурчу вот, у меня идеалы другие - множественное наследование и интерфейсы-не-классы.

Я и на плюсах писал ина джаве пишу. Доволен/не доволен - не то слово ) Просто нужно понимать, что и ради чего делалось.

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

Что у нас было?

Нужна поддержка физики? Добавили родителя CPhysic, переопределили методы. Нужно реализовать поведение ИИ? Добавили родителя CBehaviour. Нужна поддержка в скриптовом языке? Добавили родителя CScript.

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

А как сейчас? Нужна нам физика? Реализуем интерфейс IPhysic. Нужно ИИ? Реализуем интерфейс IBehaviour. Нужна поддержка в скриптовом языке? Реализуем интерфейс IScript.

Как всю эту красоту заставить работать? А у нас есть объект PhysicManager, который работает с коллекцией IPhysic и обслуживает всю физику. Аналогично с другими слоями.

Для обобщённого программирования (полиморфизма) в языках со статической типизацией.

Обобщенное программирование - это generic'и и template'ы. Полиморфизм - это концепция из ООП.

Не для удобства обращения к объектам и не для большой команды, а для обобщённого программирования.

Ну не. Интерфейс создан для сокрытия реализации. Уменьшает связность модулей.

И в какой-то степени является заменой множественного наследования.

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

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

То, что интерфейсы нельзя изменять, - не правда. Никакой негативной практики, как вы пишете, в этом нет.

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

Нужно 10 раз подумать, прежде чем вносить изменения в контракт.

А вообще это классический пример нарушения буквы O из SOLID )

Также const в интерфейсе не может быть, потому что константы не принадлежат экземплярам

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

Обобщенное программирование - это generic'и и template'ы.

Вот второй абзац в Википедии про обобщённое программирование:

Обобщённое программирование рассматривается как методология программирования, основанная на разделении структур данных и алгоритмов через использование абстрактных описаний требований[3]. Абстрактные описания требований являются расширением понятия абстрактного типа данных. Вместо описания отдельного типа в обобщённом программировании применяется описание семейства типов, имеющих общий интерфейс и семантическое поведение (англ. semantic behavior). Набор требований, описывающий интерфейс и семантическое поведение, называется концепцией (англ. concept). Таким образом, написанный в обобщённом стиле алгоритм может применяться для любых типов, удовлетворяющих его своими концепциями. Такая возможность называется полиморфизмом.

Дженерики как частный случай там есть, но ими не ограничивается. Вот например в той же статье на английском (в русском вырезано):

Similarly, dynamically typed languages, especially interpreted ones, usually offer genericity by default as both passing values to functions and value assignment are type-indifferent

Тут говорится про то, что например JS имеет "обобщённость" просто потому, что не проверяет типы.

Полиморфизм - это концепция из ООП.

Ну если вы имеете в виду изначально только ООП, тогда да. А так вот из Википедии опять:

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

------

Интерфейс создан для сокрытия реализации. Уменьшает связность модулей. И в какой-то степени является заменой множественного наследования.

Для сокрытия создано сокрытие (private). Интерфейс нужен для обобщённого программирования. Поясню ещё раз. Я например ожидаю, что аргумент функции будет иметь метод work. Мне без разницы на наследование, на то открыта реализация или нет, есть дженерики в языке или нет, мне нужен только метод work. Почему это обобщённое программирование? Потому что я написал код функции один раз, никогда его не меняю, а он работает с разными типами входящих данных. Почему это полиморфизм? Потому что разные объекты подходят в одно место использования.

В чем накладность интерфейса?

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

Нужно 10 раз подумать, прежде чем вносить изменения в контракт.

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

Также const в интерфейсе не может быть

Ну насчёт этого я перегнул. Смысл моего выпада был в том, что интерфейс это контракт. А его используют уже как склад всего подряд. Отсюда новичёк подумает, что private в интерфейсе - это так и надо. Только потом узнает, что это совсем не тот private (не контракт). Хотя в нынешние времена, насколько я понял, новичок даже не задумается, потому что для него интерфейс - это просто штука из конкретного языка, а не воплощение контракта.

Для сокрытия создано сокрытие (private).

Так вы реализацию не скроете. У вас в определении типа будет торчать вся начинка. И когда вы ее будете подключать в свой проект, все зависимости private - весело прискачут к вам.

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

Пример:

public class AIService {

private List<AIObject> chars;

private AiHelper helper;

....

public void think(float duration);

}

Сравните с интерфейсом:

public interface AI {

void think(float duration);

}

Когда вы тянете AiService в любую другую часть проекта, у вас в нагрузку прилетает и AIObject и AiHelper и List. Плюс одна головная боль. А может у вас уже есть AiObject?

А может у вас там чудесным образом циклические ссылки?

А архитектурно вам реализация не нужна. Меньше знаешь - крепче спишь.

Когда вы тянете AI в любую другую часть проекта, у вас в нагрузку не летит НИЧЕГО ВООБЩЕ.

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

В терминологический же спор вдаваться не буду - суть не меняется.

В том что его нужно писать.

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

Тогда таких интерфейсов очень много.

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

Вот я и говорю, если не нужно обобщённое программирование, то есть если метод и так всегда работает с одним классом или с одним предком, а не с любыми объектами, то интерфейс тут не нужен, а нужен класс-предок. Это экономит силы программиста.

Вот это не так. У вас может быть в проекте ровно одна реализация некого сервиса. Пусть будет AIService. В этом случае все равно полезно выделить интерфейс. ДЛЯ УМЕНЬШЕНИЯ СВЯЗНОСТИ И СОКРЫТИЯ РЕАЛИЗАЦИИ.

Пример - выше. А экономить силы программиста отказываясь от архитектурно верных решений - удел джунов.

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

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

Мне кажется, что там и будет монолит

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

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

Отсюда вытекает, что новичков нужно наставлять на путь истинный получением фундаментального образования. Математика? Кватернионы? Базисы? Аффинные преобразования? Сплайны? Теория сложности? Вы о чем, кто все эти люди? Юнити за меня все сделает )

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

Тут говорится про то, что например JS имеет "обобщённость" просто потому, что не проверяет типы.

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

>> В чём разница между ключевыми словами final, finally, finalize?

А можно услышать ответ, в контексте Unity (C#) ? )

по С# такого не нашел, но вот в Java они присутствуют (оставляю для общего развития)
Скорее проще перечислить что есть у них общего… а это только корень final и то что они являются зарезервированными словами в Java.
final — модификатор, применяющийся к классам, методам, переменным. В общем своём представлении делающий объект своего действия неизменным, а если быть точнее, то после его применения:-от класса становится невозможно наследоваться;-метод невозможно переопределить;-переменную невозможно изменить(но в случае, если переменная-это ссылка на объект, то на состояние объекта final никак не влияет, если только класс сам не является final).
finally является частью конструкции try-catch-finally, в где играет роль блока, который выполняется независимо от событий происходящих в try-catch и выполняется в любом случае.
finalize() является методом класса Object.

Я специально написал в вопросе в контексте Unity (C#). В C# вопрос должен звучать так - чем отличается readonly (и sealed) / finally / Finalize().

А еще лучше просто спросить, что такое readonly, sealed, как работает try/catch/finally (и хорошо если скажут когда не работает )), и что такое Finalize() (можно спросить в связке с Dispose()).

Про ключевое слово "final" и "checked/unchecked exceptions". Подобное разве есть в C#? Это скорее вопросы для джавистов.

вероятно это вопрос с подвохом и ответ возможен такой - в С# таких понятий нет, это Java синтаксис.
В свою очередь могут задать - "а что подобное есть в С#?"

Если вам на собесе по C# задают вопрос по Java - нужно бежать из такой конторы. Это что за самодурство такое? Если человек работал со многими языками - плюс в карму. Но если он собеседуется на шарп, а вопрос получает по плюсам или по джаве - это за гранью добра и зла. Нужны разные технологии? В одном лице? Еще один повод подумать, а надо ли работать в такой конторе. Но если уж очень надо - тогда надо указывать в требованиях к вакансии.

Про "checked/unchecked exceptions" - наверно он имел в виду обработанные catch-ем и unhandled exceptions. Вообще мне кажется автор пару вопросов из собеса по Java случайно добавил. )

Это не заменит чтение книг. Про solid в первую очередь это труды дяди боба, а не это. Например про S, что за задача? У класса несколько методов, каждый метод, очевидно, решает одну или несколько задач. Где границы какой то непонятной задачи? Тема гораздо глубже и касается причин которые могут затронут несвязанные бизнес задачи. Незнание этого приводит в запутанному коду, когда совсем разные причины затрагивают совсем несвязанные бизнес задачи соответственно. И такие блоки кода очень сложно распутывать и разделять.

Инверсия зависимости так же гораздо сложнее. Это не просто наплодил интерфейсики и внедрил реализации. Так же надо знать где определить пограничный интерфейс. И зачем в принципе тут уместно инвертировать зависимость? Чтоб бизнес логика не зависела от деталей реализации например.

Sign up to leave a comment.

Articles