Pull to refresh
17
0.5

User

Send message

Класс должен решать только одну проблему.

Пусть будет так. Для того чтобы это сделать нужно правило - как детерминировано определить какую единственную задачу должен решать класс. Где это правило?

Добавляй новую функциональность через новые классы/методы не меняя старые.

А если нужно поменять старые?

Дочерние классы должны соблюдать требования к родителю.

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

Дели большие интерфейсы на маленькие и специализированные.

Пусть будет так. А где правило согласно которого мы определяем что такое большие а что такое маленькие интерфейсы? Без этого правила принцип - это красивый и бесполезный набор слов.

 Преждевременная оптимизация

→ Попытки «предусмотреть всё» ведут к переусложнению.

→ Пример: Создание интерфейса IReportExporter для единственной реализации PDFExporter.
→ Философия: «Не создавай абстракцию, пока нет реальной необходимости» (YAGNI+KISS > SOLID).

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

В сухом остатке так и не понятно из статьи. Есть абстрактный SOLID который непонятно как использовать а если использовать то еще и куча проблем может быть и нужно 10 раз подумать. А по итогу то нужно или нет? Если нужно то где обоснование что оно имеет больше плюсов чем минусов?

PS Статья написана нейронкой а ответы хотелось бы от автора получить.

Ну, подвинуть Java - это изначально не реалистичная цель была.

Вполне реалистичная. На том же андроиде Kotlin вообще вытеснил Java. Другой пример, как я уже писал - Go. Язык, у которого куча детских болячек, у которого не было ни экосистемы нормальной ни даже пакетного менеджера, плюс который откровенно недолюбливали многие за его примитивность. Т.е. по сравнению с Kotlin у которого на старте была уже вся экосистема и инструменты, там вообще все плохо. И тем не менее, он взлетел и откуси не то что у Java а у всего пула backend языков не слабый кусок рынка.

И вот получается, есть красивый милый приятный современный Kotlin с весьма скромными результатами на уровне приза зрительских симпатий. А есть кривой косой морально устаревший Go который при этом подвинул рынок. И еще раз повторю - все потому что Go было что предложить рынку кроме своей приятности а Kotlin - нет.

Это я не к тому что не люблю Kotlin а к тому - почему для этого языка дела сейчас обстоят так как обстоят.

@devmark @ermadmi78

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

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

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

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

У Kotlin есть и плюсы и минусы перед Java.

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

Точнее, не так. У Kotlin была киллер-фича - быть мульти-языком для решения большинства проблем. Другими словами - это должен бы быть единый язык на котором бы писали и фронт и бэк и мобилку. Но идею не вывезли. KotlinJs особо никому не нужен. KMM оказался не лучшей идеей и не взлетел. Остался только KotlinJvm который по сути better java. Раньше хотя бы корутины были преимуществом, но с появление виртуальных потоков и они были нивелированы. Kotlin к сожалению сейчас ничего не может предоставить весомого чтобы било Java и было поводом перейти на этот язык.

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

Ваш же комментарий выше по поводу goto в Java:

Кто? В Java он есть, но я не видел чтобы его кто-то использовал.

Согласно Вашей же логике - нет, его там нет.

Или Вы имели в виду что в Java есть конструкции по смыслу представляющие из себя goto но называющиеся по другому? Ну окей.

Но если так, не очень понимаю претензию к моим комментариям где я пишут ровно тоже самое - в статье формально не упоминается ФП но по смыслу оно там есть?

Определитесь уже.

В статье Гётц пишет, что ФП это когда всё это функция. Наверное он так и думает. И если бы он считал, что главное в истории с рекордами и паттерн матчингом это добавление в Джаву элементов ФП он бы так и сказал.

А еще он пишет что в ООП все есть объект. В Java половина языка - это не объекты, следовательно Java вообще не ООП язык? Или всетаки нет?

Там никто не стесняется, все все понимают и даже в названиях можно это увидеть: map, filter, compose, identity, никто не стал ничего придумывать заимствовали честно. Также ни для кого не секрет что  паттерн матчинг это фича фп языков и в основе это оттуда пошло в другие языки. В ФП языка он по больше части нужен для работы с ADT. Удивительно но даже в тексте Гёца он прямо пишет про ADT, тоже одной из отличительных особенностей ФП. Но нет он не это имел ввиду разумеется, потому что не написал волшебное слово ФП.

Заходим для примера в оф документацию по Rust там не стесняясь:

We’ve already covered some other Rust features, such as pattern matching and enums, that are also influenced by the functional style. 

И только в Java добавляют все тоже самое что и все другие но нет, это с ФП не связано.

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

Полноценного ООП в Rust/Go нет и не планируется что не мешает иметь методы.

Подхода "всё это функция" в Clojure нет и не планируется, понятия чистых функций нет и не планируется, за сайд эффектами никто не следит и не собирается. Но тем не менее это типичный ФП язык.

Что мешает Java развиваться перенимая фичи из ФП но не становясь чистым ФП языком? Загадка.

Циклы заменили стримами моментально, практически за ночь.

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

When we're modeling complex entities, OO techniques have a lot to offer us. But when we're modeling simple services that process plain, ad-hoc data, the techniques of data-oriented programming may offer us a straighter path.

Where OOP encourages us to use classes to model business entities and processes, smaller codebases with fewer internal boundaries will often get more mileage out of using classes to model data

Что-то не так? Да есть ООП, да оно решает определенные задачи со всем уважением и мы с ним будем дружить и дальше но ветер перемен дует в другую сторону. Почему не написано так прямо и я интерпретировал фразу? Потому что Brian Goetz пишет весьма дипломатично и естественно не будет в открытую заявлять что нам приоритет сейчас не очень ООП. Почему я интерпретирую статью так хотя там в лоб так не написано? Очень просто, достаточно посчитать количество нововведений например начиная с Java 7 связанных с ООП и с ФП и все станет очевидно в каком направлении все движется.

Ну только Data Oriented Programming с функциональным программированием имеет примерно столько же общего, сколько с ООП. Рекорды имутабельные, имутабельность, конечно, ключевой компонент ФП, но не ключевой компонент Data Oriented Programming.

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

Да неужели. А если дальше почитать?

Algebraic data types

This combination of records and sealed types is an example of what are called algebraic data types (ADTs). Records are a form of "product types", so-called because their state space is the cartesian product of that of their components. Sealed classes are a form of "sum types", so-called because the set of possible values is the sum (union) of the value sets of the alternatives. This simple combination of mechanisms -- aggregation and choice -- is deceptively powerful, and shows up in many programming languages. 

Но это разумеется совпадение.

  • неизменяемые структуры данных +

  • паттерн матрчинг +

  • алгебраические типы данных +

  • монадические типы вроде Optional +

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

А все потому что дальновидный Brian Goetz написал "Data Oriented Programming" видимо мудро предвосхищая что напиши он ФП то у фанатов ООП случиться истерика.

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

Нужно читать комментарий на который отвечаете

Поздравляю! Вы воюете не туда.

Все намного проще. Pattern matching - это как и sealed classes, records и прочее - это история которая пришла в Java из функционального программирования.

А знаете почему? Потому что с годами оказалось что ООП не торт. Ну по крайне мере для серверного приложения, где главная задача обрабатывать данные а не состояния и там лучше подходит ФП. Java туда потихоньку двигается.

Главный архитектор Java Brian Goetz написал статью о том что ООП конечно мы уважаем и для каких то задач оно подходит но мы будем двигаться в сторону ФП.

Data Oriented Programming in Java

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

С каждой такой статьей невольно приходит мысли что современный HR это не про найм а про вредительство и саботаж.

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

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

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

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

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

Даже если человек состоит в таком сообществе, это ещё не значит, что он нарушает этику или халтурит. Судят всё же по делам, а не по подпискам.

Подобные шаги со стороны работодателя выглядят как попытка контролировать мышление сотрудников. Сегодня уволили за канал про «меркантильность», завтра — за лайк под каким-нибудь постом. Корпоративный контроль за убеждениями — это тревожный тренд, особенно в компании с гос-корнями.

Есть сотрудник работающий на ювелирной фабрике. Руководство выяснило что он подписан на паблик анонимных клептоманов.

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

А теперь реальный способ чтобы у вас была команда а не рабочая группа:

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

  • в команде не должно быть мудака-руководителя. Как бы вы все хорошо не построили, мудак-руководитель все равно все испортит

  • процессы должны создаваться из нужд команды а не команда подгоняться под процессы

  • не должно быть кумовства. Кумовство разрушит проект не так быстро как мудак-руководитель, но разрушит

  • руководитель должен быть в одной упряжке со своей командой

  • каждый член команды не должен чувствовать себя винтиком

  • все что делается командой, должно делаться зачем-то

Давайте, покажите, какие издержки у этой "виртуальной машины". Что там "тормозит". По какой причине код шарпа в принципе не может быть быстрее c++/rust.

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

Почему для C# проблемно быть на уровне Rust/C++?

Если представим что в C# нет сборщика мусора то причина в природе его работы. Rust/С++ значительно проще, для них прямой вызов это прямой вызов, им не нужны переходника для методов, у них нет ленивой загрузки, валидации, нет динамики. CLR из-за своей природы вынуждена делать кучу доп вычислений: JIT, поиски и загрузки, подмены переходников, валидацию, оптимизации и прочее прямо во время выполнения программы. Rust/C++ делают все что им нужно во время компиляции и практически не ограничены во времени и поэтому могут применять оптимизации любой сложности. В тоже время VM язык ограничен во времени оптимизации очень сильно.

VM языки умеют во время выполнения использовать особенности конкретного процессора и т.д. прекрасно. Делаете PGO для Rust/C++ и получаете тоже самое. Что дальше то? Что еще могут предоставить VM языки, считающие все и вся по ходу выполнения?

Разумеется есть у них тузы в рукаве, но эти тузы не про производительность.

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

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

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

process virtual machine, sometimes called an application virtual machine, or Managed Runtime Environment (MRE), runs as a normal application inside a host OS and supports a single process. It is created when that process is started and deleted when it is closed. Its purpose is to provide a platform-independent programming environment that abstracts away details of the underlying hardware or operating system and allows a program to execute in the same way on any platform.

Кто у нас запускает приложение? Само? Нет. Мы запускаем виртуальную машину и уже она загружает приложение и готовит его к запуску. Как виртуальная машина исполняет приложение: интерпретирует(Python), либо интерпретирует с элементами JIT компиляции(JS/Java), либо полностью JIT компилирует. Это не важно, это средства реализации со своими плюсами и минусами.

Приложение запускается,происходит загрузка и JIT необходимых метаданных, методов и точки входа Main().

Пруф

Before an object instance is created, the CLR looks up the loaded types, loads the type if not found, obtains the MethodTable address, creates the object instance, and populates the object instance with the TypeHandle value. The JIT compiler-generated code uses TypeHandle to locate the MethodTable for method dispatching. The CLR uses TypeHandle whenever it has to backtrack to the loaded type through MethodTable.

Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects

Запускается Main(). Действительно теперь код работает почти как нативный. Дальше в ходе выполнения нужно вызывать метод другого класса который еще не загружен. Что делать? Правильно, Main() передаст управление виртуальной машине которая пойдет искать новый класс в метаданных сборки, затем они будет его верифицировать и загружать в память (мне никто не мешает пойти в IL код и руками сделать там все что угодно после того как оно скомпилирован, поэтому VM вынуждена проверять все в первый раз).

Пруф

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

Процесс управляемого выполнения

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

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

Пруф

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

Процесс управляемого выполнения

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

Скрытый текст

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

Процесс управляемого выполнения

Ах да, места вызовов. Допустим у нас есть класс A и метод fu() и он уже JITован. Далее мы вызываем метод bar() класса B. Если B не загружен мы вынуждены загрузить его. И вот проблема, у B весь код сырой в формате MSIL и ничего не знает про A. После загрузки B и JIT компиляции и вызова bar() он дойдет до fu(). Проблема в том что у bar() нет информации о том где лежит fu() и он вынужден ... искать этот метод в A и только потом менять переходник у себя. Выходит что мало просто один раз вызывать метод чтобы он JITанулся, нужно еще чтобы каждый целевой метод хотя бы раз его вызвал и поменял у себя переходники. Да, когда все они друг друга навызывают то код начнет работать почти как нативный. Почему почти? Потому что нативному коду это все не нужно, у него все посчитано при компиляции, ну максимум методы динамической библиотеки подключить да, у любого же языка с VM есть различные издержки в необходимости держать переходники и производить разрешения методов и метаданных. Такова их природа со своими плюсами и минусами.

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

Нет в дотнете виртуальной машины.

Лол нет. Во время компиляции. Виртуальной машины не существует.

Официальный сайт компании Microsoft.
Официальный .NET глоссарий.

https://learn.microsoft.com/ru-ru/dotnet/standard/glossary
https://learn.microsoft.com/en-us/dotnet/standard/glossary

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

Среда CLR обрабатывает выделение памяти и управление ею. Среда CLR также является виртуальной машиной, которая не только выполняет приложения, но и создает, а также компилирует код с помощью JIT-компилятора.

A CLR handles memory allocation and management. A CLR is also a virtual machine that not only executes apps but also generates and compiles code on-the-fly using a JIT compiler.

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

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

Неужели? Ну хорошо, вот мои тезисы человека несомненной непонимающего ничего.

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

Накладные расходы на поиски различных метаданных. К примеру когда метод вызывается в первый раз, его для начала нужно вообще найти, быть может его вообще не существует. После того как он найден, нужно проверить его корректность. Далее нужно провести JIT-компиляцию, скорректировать все метаданные и переходники т.д. Далее каждый вызов будет происходить через переходники а не напрямую. Не поверите, но все эти манипуляции далеко не бесплатные. И это все описание "прямого вызова". Вызов интерфейсного метода будет сопровождаться куда более увлекательной беготней по itables.

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

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

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

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

Если для вас векторные инструкции вроде AVX - это секретные оптимизации, то я вас слегка огорчу. Они ни для кого не секретные.

Вот именно что ни для кого. Удивительно что по Вашей логике языки на VM в это чудо умеют а нативные - нет. А еще удивительней, что VM языки настолько в это умеют что в C# целый API сделан для ручной работы с векторными инструкциями и нигде не пишут мол - выбросьте из головы, чудо JIT сам решит все ваши проблемы. А в Java, учитывая что там есть векторизация на уровне JIT, почему то пилят ручной Vector API, интересно, зачем они это делают если должно произойти чудо и все само должно оптимизироваться?

Вообще не факт. JIT-компилятор ориентируется на платформу, на которой запущен и может оптимизировать код под фичи процессора.

Старые истории про волшебный JIT который секретные оптимизации делает в рантайме и виртуалка начинает превосходит нативные языки. Что в Java что в C# одни и те же истории. Только почему то все забывают про серьезные накладные расходы самой виртуальной машины, которые для начала ей нужно как то компенсировать. И почему то никто не задумывается что для нативного языка тоже можно сделать PGO.

Спасибо за статью.

И старые посылы, что не стоит писать нагруженный код на C# уходят в прошлое.

Смотря что подразумевать под "нагруженный код". С одной стороны, C# всегда был богат инструментами для серьезных оптимизаций и выжать можно было много. С другой там где это по настоящему важно он будет сливать С++, Rust etc хоть с AOT хоть без него.

Information

Rating
2,015-th
Location
Санкт-Петербург и область, Россия
Registered
Activity

Specialization

Backend Developer
Lead
C#
Java
Rust
Golang
Multiple thread
C
System Programming
Game Development
Unity3d
Algorithms and data structures