SOLID на примере JavaScript с chatGPT -7


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

Запрос:

write me javascript code with solid principles and comment code

Ответ:

// Single Responsibility Principle: Каждая функция должна иметь одну и только одну ответственность
function getUserById(userId) {
  // Эта функция отвечает за получение пользователя по его ID
  return fetch(`https://example.com/users/${userId}`)
    .then(response => response.json())
    .then(user => {
      if (!user) {
        throw new Error(`User with ID ${userId} not found`);
      }
      return user;
    });
}

// Open/Closed Principle: Модуль должен быть открыт для расширения, но закрыт для модификации
class UserService {
  constructor(userRepository) {
    // Класс UserService закрыт для модификации, но его можно расширить, передав другую реализацию userRepository.
    this.userRepository = userRepository;
  }

  getUserById(userId) {
    return this.userRepository.getUserById(userId);
  }
}

// Тут по подробней описал снизу, чтобы окончательно понимать суть принципа...
// Liskov Substitution Principle: Подтипы должны иметь возможность заменять свои родительские типы без изменения корректности программы.
class CachedUserRepository {
  constructor(userRepository) {
    this.userRepository = userRepository;
    this.cache = new Map();
  }

  getUserById(userId) {
    // CachedUserRepository является подтипом UserRepository и может использоваться в качестве замены, не влияя на корректность программы.
    if (this.cache.has(userId)) {
      return Promise.resolve(this.cache.get(userId));
    }
    return this.userRepository.getUserById(userId).then(user => {
      this.cache.set(userId, user);
      return user;
    });
  }
}

// Interface Segregation Principle: Не следует заставлять модуль реализовывать интерфейсы, которые он не использует.
class UserController {
  constructor(userService) {
    this.userService = userService;
  }

  getUserById(req, res) {
    const userId = req.params.id;
    this.userService.getUserById(userId)
      .then(user => res.json(user))
      .catch(err => res.status(404).json({ error: err.message }));
  }
}

// Dependency Inversion Principle: Модули высокого уровня не должны зависеть от модулей низкого уровня.
const userRepository = new UserRepository();
const cachedUserRepository = new CachedUserRepository(userRepository);
const userService = new UserService(cachedUserRepository);
const userController = new UserController(userService);

Если вкратце у нас есть userRepository будь то файл или модуль, в котором есть функция getUserByID, что уже является принципом "S".

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

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

Запрос:

tell me about Liskov Substitution Principle more in this example

Ответ:

Принцип замены Лискова (LSP) гласит, что подтипы должны иметь возможность заменять свои родительские типы без изменения правильности программы. В этом примере класс CachedUserRepository является подтипом класса UserRepository, и его можно использовать в качестве замены для UserRepository, не влияя на корректность программы.

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

У класса CachedUserRepository есть конструктор, который принимает экземпляр UserRepository в качестве аргумента и использует этот экземпляр для фактического вызова API, когда в кеше нет пользователя.

В этом примере класс CachedUserRepository соблюдает принцип подстановки Лискова, поскольку он имеет тот же метод, что и класс UserRepository, он просто добавляет еще одну функцию кэширования и не меняет поведение или возвращаемое значение метода getUserById.

Следовательно, везде, где в приложении используется UserRepository, CachedUserRepository также можно использовать в качестве замены без нарушения существующего кода.

"I" - принцип разделение интефейсов гласит, что модуль не следует заставлять реализовывать интерфейсы, которые он не использует.

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

Класс UserService может иметь другие методы, такие как getAllUsers, createUser, updateUser и т. д. Но класс UserController использует только getUserById, ему не нужно реализовывать все методы и ему не нужно знать о существовании других методов.

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

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

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

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

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




Комментарии (8):

  1. lair
    /#25152042 / +9

    Если вкратце у нас есть userRepository будь то файл или модуль, в котором есть функция getUserByID, что уже является принципом "S".

    Нет, не является.

    Класс UserService может иметь другие методы, такие как getAllUsers, createUser, updateUser и т. д. Но класс UserController использует только getUserById, ему не нужно реализовывать все методы и ему не нужно знать о существовании других методов.

    ...а какая связь между тем, какие методы UserController использует, а какие - реализует?

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

    При чем тут ISP? Почему это не SRP?

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

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

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

    Каким образом передача другого репозитория расширяет класс UserService?

    "D" - принцип инверсии зависимости, гласит что классы не могут быть зависемы от класс низкого уровня

    Что, простите?

    Впрочем, у вас и пример на DIP такой же непонятный, как ваше объяснение.

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

    Ваша статья, к сожалению, это никак не демонстрирует.

  2. panzerfaust
    /#25152108 / +8

    Я уже в ожидании статей "SOLID и квантовые вычисления", "SOLID и Stable Diffusion", "SOLID и Принц-полукровка"

  3. AllRight88
    /#25152268 / -1

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

    Я бы прокомментировал его так. То что при изменении объекта более низкого уровня не требуется менять объект более высокого. То есть он все-таки зависим (он зависит от интерфейса, но не от реализации).

    • dopusteam
      /#25154210 / -1

      При изменении объекта более высокого уровня, изменять объект более низкого уровня тоже не требуется

      • lair
        /#25154384

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

  4. ShadowOfCasper
    /#25159500

    Данный пример демонстрирует как раз обратное. ChatGPT не лучший способ для демонстрации принципов SOLID. Взятая для примера композиция никак не отразает принцип Dependency Inversion.
    Гораздо нагляднее это может показать абстрактный класс который может наследоваться простым, но при этом нельзя сделать наоборот - абстрактный класс не может наследовать простой. В данной композиции роль абстрактрого класса играет голая функция, что вообще неочевидно.

  5. Sipaha
    /#25161334

    Single Responsibility Principle: Каждая функция должна иметь одну и только одну ответственность

    Принцип не об этом. Такое определение ближе к истине:

    The single-responsibility principle (SRP) is a computer programming principle that states that "A module should be responsible to one, and only one, actor." The term actor refers to a group (consisting of one or more stakeholders or users) that requires a change in the module.

  6. Asisyas
    /#25161338

    Круто. Нужно было просто сказать «юзайте интерфейсы, с помощью di делайте декорации на их основе. А чтобы провайдить функционал из сервис-леера в контроллеры, юзайте интерфейс фасадов сервис-леера :)

    Только непонятно, что нового открыл для нас этот AI чего раньше не было в туториалах/литературах ?)