Pull to refresh
2
0
Nick Gushchin @iNikNik

Software Engineer

Send message
Я согласен с вами, но с оговоркой, что это очевидно когда вы отрыли исходники и смотрите на эти три строчки кода (т.е. на уровне деталей реализации).

Если же вы работаете с методом интерфейса, то для вас не очевидно что произойдет (в плане сайд-эффектов, да и в целом).

public interface HttpHandler {
  public Response handle(req: Request)
}


Только подебажив (или запустив тесты), вы увидите, что он и логирует и обрабатывает.

Вы согласны с этим?
Он ничего не делает сам.


Не очень понял. Он как минимум:
  1. Логирует
  2. Инициирует работу внешнего сервиса
  3. Обрабатывает запрос (из этого вытекает п.2). Имеется ввиду цикл req -> res


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


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

Если хотите логировать, пожалуйста, делайте интерфейс для логирования и реализуйте его и также агрегируйте объект этого интерфейса


Модуль получает запрос, производит с ним какие-то вычисления, возвращает ответ. Задача — сделать так, чтобы запросы логгировались, не нарушая принципы солид.

public interface Request {}
public interface Responce {}

public interface HttpHandler {
  public Response handle(req: Request)
}


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

Мне в голову приходит такой вариант, но он решительно нарушает СОЛИД (как я понял с ваших слов):
public class MyHttpEndpoint extends HttpHandler {
    // внешняя зависимость
    Logger log;

    public Response handle(req: Request) {
      // делегируем логгеру грязную работу
      log.info("Some logging stuff here");

      Integer result = doJob(...); // 42
      Response response = new JsonHttpResponse(...);

      return response;
    }
}
Вы привели архитектуру, которая подразумевает, что:
а) У нас известны все возможные кейсы реализации, чтобы учесть все возможные сценарии в изначальном интерфейсе
б) Это дело пишется внутри одной команды и люди могут оперативно вносить изменения если понадобилось внести какую-то новую функциональность
в) Это все никак не отменяет поинта с интерфейсом. Просто попробуйте в Вашей интерпретации СОЛИДА сделать функционал, который получает какие-то абстрактные данные (кафка, http-запрос), обрабатывает их и логгирует, чтобы при этом каждый из компонентов делал только одно.
В первой статье автор проводит сравнение по количеству классов (типов) созданных при реализации в ФП и ООП… хм.

Перед этим приводит в качестве аргументов, что в не ФП языках есть ФП фичи… хм.

это одни и те же грабли только с разных сторон


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

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


Создатели Swift не стали пугать программистов монадой. Они знают, что такое монада, и ввели в язык синтаксис, который выполняет монадические вычисления: последовательность шагов в контексте с отсутствием значения.


Вызывает только ироничную улыбку. Т.е. по мнению автора — монады, это примерно эквивалентно синтаксическому сахару.

Хочу лишь сказать, что если Вы составляли свое мнение о ФП на основе этих материалов — искренне рекомендую обратиться к более компетентным источникам (Вы будете удивлены).

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


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

Противоречие номер один — в том, что в ООП есть именно наследование (один класс расширяет другой класс, не интерфейс!), а использовать этот механизм — чуть ли не антипатерн.

вместо этого просто агрегируйте или делайте композицию объектов.


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

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


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


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


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

Корректно. Зависит — это не тоже самое, что и Использует без показа этой зависимости. В вашем случае, вы просто в имплементации стали Использовать схему, вопрос почему? в Архитектуре её нет. Это просто прихоть программиста.


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

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

Я тогда не понимаю, если с данными работаете, зачем схема? Пусть она и работает с данными, а вы сделайте над ней обертку, где будет и схема и id и выбирайте в ней все что вам надо, а потом отдавайте только данные. Отдельный класс со схемой.


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

interface SchemaRegistryClient {
  Schema getSchemaById(int id);
}

class AvroDeserializer<T> extends Deserializer<T> {
    SchemaRegistry registryClient;

    public AvroDeserializer(SchemaRegistryClient registryClient) {
        this.registryClient = registryClient;
    }

    @Override
    public T deserialize(String topic, byte[] bytes) {
        int id = readSchemaId(bytes);
        Schema schema = registryClient.getSchemaById(id);

        // do some deserialization stuff using provided schema
        final T message = ...;

        return message;
    }
}


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

Вы же не предлагаете под каждую схему данных делать отдельную очередь сообщений (KafkaForAvro, KafkaForJson)? Надеюсь, что нет.

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

Их всего три, они основные, но как и в любой другой отрасли — это лишь основа, а есть еще уточнения. Уточнения не противоречат основе. Это как Конституция и другие законы.

Поэтому ООП это не только три кита, но и как вы верно сказали, СОЛИД, КИСС и так далее...


Я не критикую и ничего не писал про СОЛИД, КИСС и другие. Мой комментарий был про то, что принципы, которые декларирует ООП (которые лежат в базе ООП подхода) — значительно сужает предметную область программ, которые могут быть реализованы на их основе. И проиллюстрировал почему.

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

PS наследование считается плохой практикой и его применения необходимо максимально избегать (это следует из тех уточнений, про которые вы говорили), заменяя на композицию. А как я писал в другом комменте — ООП не подразумевает композиционности, только композируемость. Не противоречие ли это?
Грубо говоря, когда делали архитектуру — не предполагали, что кто-то будет туда передавать схему.


Нет нет, дело совсем не в этом. Дело в том, что конкретно этому коду все равно есть ли там схема и какая она. Он работает с данными. С массивом байт. А на выходе он хочет получить объект типа T (при этом — любого. На тип не наложено никаких ограничений. В частом случае метод десериализации может вернуть массив байт без изменений и это будет считаться валидной реализацией).

Сейчас класс Deserializer зависит только от буфера данных (заметьте от id он тоже не зависит), от строки и SchemaRegistry. Больше ни от чего, все остальное — программист может реализовывать и делать все что ему в голову придет. Это просто костыль.


Не корректно. Возьмите любой интерфейс любого фреймворка, который позволяет создавать REST-API. Все, о чем там упоминается — есть запрос, нужен ответ. Там есть упоминания про MySql? про Rabbit? Нет? Значит неверная архитектура? Если наш класс зависит от двух других — это неверная архитектура?

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

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

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

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

Отдельно отмечу, что если почему-то есть ощущение, что солид нарушается — то это только потому, что потому что я привел две части совершенно независимых модулей, как будто они были сделаны одним разработчиком (да еще и упаси боже — мной) и лежат в виде 3х (4х\5ти) файлов в одной папке. Все не так. В 100й раз подчеркну, есть отдельный модуль\пакет\библиотека для получения данных из кафки. Есть отдельный модуль\пакет\библиотека для работы с авро. Есть отдельный модуль\пакет\библиотека для работы с реджистри. Есть уже конкретный модуль\пакет который пишет конечный разработчик, который создает продукт (часть продукта) и он использует вышеперечисленные кусочки кода (опенсорс) в своей программе. В каждом таком модуле соблюдены принципы SOLID (и др.) на достаточном уровне. Единственное, почему Вам может показаться, что собранная из этих кусочков система не соблюдает какие-либо принципы (придуманные умными людьми), так это потому, что ООП подход по определению подразумевает, что части программы (классы, интерфейсы или целые модули) не композициональны (тут очень сложно перевести на русский язык. В оригинале это «composable != compositional»). То есть их можно скомпоновать, чтобы использовать вместе, но их нельзя рассматривать как части композиции. Но это еще один камень в огород ООП и совсем другая история.

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


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

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

Подводя итоги оставляю открытый вопрос — оправданы ли проиллюстрированные абстракции с точки зрения желаемого результата?

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


Это не моя задача, строго говоря. За пример был взят код, который дебажил, в тот день когда написал комментарий. Библиотеки на которые я ссылаюсь — опенсорсные, их писали умные, талантливые люди и у меня нету к ним претензий. Я более чем уверен, что они реализованы с соблюдением всех солидов и прочего. Я просто ставлю под сомнение изначальные парадигмы (ООП подход) и искренне считаю, что он был выбран не потому, что он лучше всего подходит для решения данной задачи, а потому что целевая аудитория больше.

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

public interface Deserializer<T> {
  T deserialize(String topic, byte[] data);
}


<T> T receiveMessage(byte[] data) {
  final Deserializer<T> deserializer = myDeserializerFactory.getDeserializer(
    getClassName<T>()
  );
  final T message = deserializer.deserialize(data);
        
  return message;
}


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

Да и в целом — мы уже ушли в сторону. Любой интерфейс подразумевает, что возможны несколько валидных имплементаций, которые удовлетворяют всем формальностям (KISS, SOLID, DRY, ...) и при этом их логика будет значительно отличаться. Соглашусь, что абстракции предложенные автором статьи позволяют уменьшить сложность разработки (разработчик в какой-то момент декларирует, что тут — черный ящик), но вот все, что касается дальнейшего жизненного цикла системы — значительно усложняется. Я имею ввиду рефакторинг, добавление новых фич и прочее и прочее.

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

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

Это то, что «должно самодостаточно работать так, как задумано, без необходимости смотреть вовнутрь».


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

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


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


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

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


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

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

Я не очень в Java и тонкостях вашей задачи, поэтому, например, не понимаю, что такое массив байт byte[] bytes и почему в нем хранится id.


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

Ну либо просто передавать её в метод deserialize, раз вы topic и массив байт туда передаете.


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


Я сознательно упростил исходный пример, но давайте тогда вернем детали. У нас есть SchemaRegistyClient, который занимается непосредственно загрузкой схем. Он передается как зависимость в наш десериалайзер (скажем — через конструктор). Чтобы быть еще ближе к реальности — схема загружается по id, который содержится в первых 4х байтах сообщения.

interface SchemaRegistryClient {
  Schema getSchemaById(int id);
}

class AvroDeserializer<T> extends Deserializer<T> {
    SchemaRegistry registryClient;

    @Override
    public T deserialize(String topic, byte[] bytes) {
        int id = readSchemaId(bytes);
        Schema schema = registryClient.getSchemaById(id);

        // do some deserialization stuff using provided schema
        final T message = ...;

        return message;
    }
}


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

нарушается принцип единственной ответственности.


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

Не надо лезть во внешнее хранилище в функции, которая занимается десерелизацие


Начну с контрпримера:

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

Integer handleResponse(Response response) {
  System.out.println(response.meta.stringify);
        
  // do stuff
        
  return 42;
}


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

Как же нам сделать, чтобы этот принцип не нарушался? Может быть сделать функцию с названием logAndHandle? Тоже нет, потому что это те же два действия — двойная ответственность. И даже если мы сделаем `Logger` и делегируем ему вывод в консоль (вы же понимаете, что я упростил свой пример с десериалайзером и на деле обращением в реджистри занимается отдельный компонент, но вызов все равно происходит в этом месте):

Logger log;

Integer handleResponse(Response response) {
  log.info(response.meta);

  // do stuff
        
  return 42;
}


То это тоже не изменит ситуацию — мы по прежнему делаем два дела. Так получается, что этот принцип соблюсти невозможно? Вовсе нет. Единственная ответственность != одно действие. Вопрос только в том, что мы понимаем под этой ответственностью.

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

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

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


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

Поясню на реальном примере: допустим мы разрабатываем систему, которая получает сообщения из кафки и что-то с ними делает. Сообщения хранятся в определенном формате (avro) и чтобы начать с ними работать — необходимо их предварительно десериализовать (для этого нужно обработать массив байт по какой-либо схеме). Описываем интерфейс:
public interface Deserializer<T> {
  T deserialize(String topic, byte[] data);
}


И вот примерно так выглядит наша функция получения сообщения:
<T> T receiveMessage(byte[] data) {
  final Deserializer<T> deserializer = myDeserializerFactory.getDeserializer(
    getClassName<T>()
  );
  final T message = deserializer.deserialize(data);
        
  return message;
}


Что-ж, казалось бы, что цель достигнута — сложная логика десериализации «спрятана в черную коробку» с помощью интерфейса и сложность системы существенно снижена. Если бы не одно НО:
— мы понятия не имеем, что происходит внутри вызванного метода.
Вот два рабочих кейса:

1. У нас есть класс, который выполняет десериализацию по заранее заданной схеме:
class SimpleAvroDeserializer<T> extends Deserializer<T> {
    Schema getSchema(String topic) {}

    @Override
    public T deserialize(String topic, byte[] bytes) {
        Schema schema = getSchema(topic);
        
        // do some deserialization stuff
        final T message;
        
        return message;
    }
}


2. В какой-то другой части проекта мы стали использовать внешнее хранилище для схем (SchemaRegistry. Представляет собой отдельное хранилище, куда попадают схемы всех сущностей проекта) и написали свой десериалайзер:
class SimpleAvroDeserializer<T> extends Deserializer<T> {
    Map<String, Schema> schemaCache;

    Schema getSchema(String topic) {}

    @Override
    public T deserialize(String topic, byte[] bytes) {
        // get schema from cache
        Schema schema = getSchema(topic);

        // if schema is not there -- load it via HTTP call
        if (schema == null) {
          schema = loadSchemaFromRegistry(topic);
        }
        
        // do some deserialization stuff
        final T message;
        
        return message;
    }
}





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

Далее нелегкая переносит нас в шкуру разработчика, которому дали задачу то-ли пофиксить баг, то-ли отрефакторить и добавить новый функционал в функцию receiveMessage. Открываем ее код и видим следующий вызов:
final Deserializer<T> deserializer;
final T message = deserializer.deserialize(data);

И с ужасом осознаем, что для того, чтобы понять, что же делает эта строчка (для того, чтобы далее провести безопасный рефакторинг) с вызовом метода интерфейса нам нужно будет изучить исходники всех его имплементаций и молится, чтобы не появились новые. Потому что сегодня там может быть обычная чистая функция (pure function), завтра — блокирующий запрос к API (наверное автор метода receiveMessage не предполагал, что основной поток получения сообщений из кафки будет заблокирован на такое долгое время), а что будет послезавтра? Никто сказать не сможет.

Вывод какой — на бумаге мы получаем «существенно сниженную сложность разрабатываемой системы», тк. принцип

наследование должно использоваться только как реализация отношения «является»


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

Подводя итоги оставляю открытый вопрос — оправданы ли проиллюстрированные абстракции с точки зрения желаемого результата?

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


Полностью поддерживаю! Сам прошел путь от процедурного программирования к ФП и теперь ясно осознаю, что, действительно, там, где раньше вместо ООП и ничего нельзя было представить — теперь очевидно (мне), что его использование выливается в кучу костылей.

не пишу гуи (и никогда не писал фронтенд)


С некоторыми оговорками — ФП тоже хорошо заходит (что на нижнем слое все же прослойка из ООП для производительности).

Ну, вот gamedev на мой взгляд довольно хорош в рамках ООП.

Information

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