LINUX.ORG.RU

Про final в Java замолвите слово

 , , ,


0

1

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

Первое - классы. Если опираться на трактовку Open Closed принципа, то мы должны иметь возможность отнаследоваться от класса, чтобы расширить его функциональность, потому final классами должны быть как правило только утилитные классы, либо то, что логически по тем или иным причинам не должно использоваться как родитель чего-либо.

Методы - а вот тут уже интереснее, опять же возвращаемся к трактовке Open Closed, правильно ли я понимаю, что все методы класса должны быть помечены как final? Т.е. даже если кто-то отнаследуется от класса, он сможет только докинуть своих методов, но не сможет переопределить то, что уже имеется. Такой же вопрос к методам-реализациям. Если класс, допустим, является реализацией какого-либо интерфейса, должны ли Override методы из этого интерфейса быть final?

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

public void foo(String s) {
    s = "New String";
}

Соб-но, дискасс, а где и как ты, ЛОРовец, используешь final и как видишь данную ситуацию?

★★★★

Ответ на: комментарий от deep-purple

Ну да. И в Gtk тоже. С учётом того, что там очень забористое наследование на pure-C.

mono ★★★★★
()
Ответ на: комментарий от mono

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

morse ★★★★★
()
Ответ на: комментарий от morse

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

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

Блин, история движется по кругу: ООП снова как модуль из 1970-х, интерфейс кнопок (снова плоские как в CDE), однозадачные ОС для docker'а...

monk ★★★★★
()
Ответ на: комментарий от TheAnonymous

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

deep-purple ★★★★★
()
Последнее исправление: deep-purple (всего исправлений: 1)
Ответ на: комментарий от deep-purple

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

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

TheAnonymous ★★★★★
()
Ответ на: комментарий от TheAnonymous

тип может быть абстрактным

Прекрати вбрасывать ООП сахарок. Не абстрактным, а базовым.

что это за класс (тип)

/* init */ objWithoutMethods->type = SOME_TYPE_ENUM;

заранее такого класса может не быть

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

потом сделали другую реализацию

Ну, дописали еще типов данных и функций для них. Не переписали, а дописали.

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

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

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

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

/* init */ objWithoutMethods->type =

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

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

Модуль, библиотека

TheAnonymous ★★★★★
()
Ответ на: комментарий от TheAnonymous

Ну, осталось придумать таблицу виртуальных методов

Она тебе понадобилась, я только показал как.

Модуль, библиотека

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

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

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

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

TheAnonymous ★★★★★
()
Ответ на: комментарий от TheAnonymous

Без ООП: функция из первого передаёт данные в функцию базового потока, второй расширяет тип и предоставляет функции для этого типа.

deep-purple ★★★★★
()
Ответ на: комментарий от TheAnonymous

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

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

функция из первого передаёт данные в функцию базового потока

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

TheAnonymous ★★★★★
()
Ответ на: комментарий от TheAnonymous

Базовая ф-ция ничего не кастит. Она вообще не трогает «расширятельные» проперти. Кастят (для себя, на время работы) только те ф-ции, которые знают что делать. Передать им управление можно:

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

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

deep-purple ★★★★★
()
Ответ на: комментарий от Jefail

Но у нас нет ни родителей ни потомков. Не ООП. Где ты их увидел?

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

а) и свитч/условие должно быть на каждую реализацию (подтип/подкласс/потомка), уже fail, потому что про них может быть тупо не известно
б) а это и есть таблица виртуальных методов. Собственно, сахар дальше вообще не нужен, вон в том же GTK вполне себе ООП, хоть и на си, сделано примерно так

TheAnonymous ★★★★★
()
Ответ на: комментарий от TheAnonymous

уже fail, потому что про них может быть тупо не известно

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

а это и есть таблица виртуальных методов

Ну, как-бы да и как-бы нет. Главное, что мы друг друга поняли.

deep-purple ★★★★★
()

Тред - перепись IT-луддитов/неосиляторов? Как будто кто-то заставляет использовать наследование. Не хотите разгребать чужое болото? Меняйте место работы или профессию. Что ни дай - всё плохо.

yyk ★★★★★
()

OCP говорит о двух вещах.

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

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

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

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

nguseff
()
Ответ на: комментарий от deep-purple

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

Если я правильно понял эту трактовку, то у тебя базовая функция явно знает о своих «расширениях», что по мне в корне неправильно.

Jefail ★★★★
() автор топика
Ответ на: комментарий от Jefail

по мне в корне неправильно

Ты сам так считаешь или тебе в умных книжках про ООП такое мнение навязали?

Смотри, какой ужас — «базовый» свич знает о кейсах!!!

switch (x) {
    case 1:
        somethingForOne();
    break;
    case 2:
        somethingForTwo();
    break;
}

deep-purple ★★★★★
()
Последнее исправление: deep-purple (всего исправлений: 1)
Ответ на: комментарий от deep-purple

Ты подменяешь понятия «базовый». Если у тебя есть ClassA, от которого наследуются ClassB и ClassC, то ClassA не может и не должен ни знать о них, ни в каком-либо виде их использовать. Причём тут свитч? Это вообще оператор, если ты делаешь switch на Enum, то он знает о всех кейсах Enum, но Enum нельзя наследовать и поэтому здесь не может возникнуть разногласий.

Jefail ★★★★
() автор топика
Ответ на: комментарий от morse

Да, ты прав. Нужно всегда помнить, что это не круто.

mono ★★★★★
()
Ответ на: комментарий от Jefail

Ты подменяешь понятия

Я ничего не подменяю. Это ты всё еще пытаешься натянуть сову на глобус ООП-шные понятия на функции с кучками данных.

у тебя есть ClassA, от которого наследуются ClassB и ClassC

У меня нет классов. Есть только кучки данных и функции.

родительский класс не может и не должен ни знать о потомках, ни в каком-либо виде их использовать

Вот эта ветвь будет в контексте ООП. О, эта мантра... Кто тебе такое сказал? На каком основании?

Причём тут свитч

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

Enum нельзя наследовать и поэтому здесь не может возникнуть разногласий

Отлично. А я ничего и не наследую, я просто вызываю нужные функции в соответствии с «типом» набора данных.

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

man полиморфизм. Тот самый кейс, когда ты можешь закладываться на контракт, что у базового класса есть определенная куча методов, которые ты можешь дергать, всё остальное - это не контракт, у одного наследника может быть +4 сверху, у другого может не быть. Если ты явно закладываешься на такие вещи в своём коде, то либо я дурак, либо ты ничего не знаешь про архитектуру. Можно? Да, можно и х*й дверью сломать, вот только это не правильно.

Jefail ★★★★
() автор топика
Ответ на: комментарий от Jefail

у одного наследника может быть +4 сверху

И что? Для дополнительной кучки данных может быть +4 ф-ции сверху, которые не будут дёргаться из той верхней где свитч.

либо я дурак, либо ты ничего не знаешь про архитектуру

Я знаю больше архитектурных решений, а ты только одно. Ну, теперь и ты знаешь, просто тебе оно не нравится.

это не правильно

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

deep-purple ★★★★★
()
Ответ на: комментарий от morse

Наследование от необстрактного класса означает что ты мешаешь вместе реализацию и вызов

Как будто это что-то плохое.

с архитектурной точки зрения

Что-то должно быть для чего-то. Т.е. «нельзя мешать реализацию и вызов» определяет какую-то цель. Если мы смешивает реализацию и вызов, но цель при этом всё равно достигается, то с «архитектурной точки зрения» всё ок.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Цель — получить хорошо модулированный и поддерживаемый код. Все архитектурные правила, естественно, эмпирические. Но нарушая их ты должен хорошо понимать что ты делаешь и зачем. И это относится к ним всем. Можно ли тащить DA-объекты на уровень визуализации? Конечно можно. Но лучше не надо.

morse ★★★★★
()
Ответ на: комментарий от Jefail

Я вот ещё что спросить хочу:

А можешь привести пример ООП кода, когда родитель «знает о потомках» (не суть, может и не знать, главное дальше читай) и может внутри базового метода сначала отработать свои дела, а потом вызвать дела потомка (а не как обычно, сперва потомок, а только потом родитель), в контексте которого и происходил вызов переопределенного(?) метода?

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

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple
(define parent%
  (class object%

    (define/pubment (foo)
      (println "Doing my own parent business")
      (inner (void) foo))

    (super-new)))

(define child%
  (class parent%
    
    (define/augment (foo)
      (println "Doing some child business"))

    (super-new)))

(define c (new child%))
(send c foo)
anonymous
()
Ответ на: комментарий от Jefail

Неправильно трактуешь. Виртуальные методы как раз про extension. Другое дело, что они правильно работать с контрактами базового класса

anonymous
()
Ответ на: комментарий от deep-purple

Паттерн «шаблонный метод»?

anonymous
()

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

Он, кстати, автор концепции проектирования по контракту (DbC).

anonymous
()
Ответ на: комментарий от anonymous

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

Конечно. Добавляй явно имя модуля и используй. Хотя про турбо паскаль не уверен, в Free Pascal точно работает

monk ★★★★★
()

Первое - классы. Если опираться на трактовку Open Closed принципа, то мы должны иметь возможность отнаследоваться от класса, чтобы расширить его функциональность, потому final классами должны быть как правило только утилитные классы, либо то, что логически по тем или иным причинам не должно использоваться как родитель чего-либо.

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

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

Legioner ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.