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

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

https://manteycaleb.medium.com/solid-design-principles-in-javascript-50ff0d2b75b5

Вы также можете получить доступ к полному примеру кода на моем GitHub.

https://github.com/Caleb-Mantey/solid-design-principles-in-js

В нашей предыдущей статье у нас был код, который выглядел так:

почтовая программа

class Mailer{
        constructor(mail, mailerFormats){
            this.mail = mail
            this.mailerFormats = mailerFormats
            this.smtpService = new MailerSmtpService()
        }         
        send(){
            // Loops through mail formats and calls the send method
            this.mailerFormats.forEach((formatter) =>          this.smtpService.send(formatter.format(this.mail)))
        }
    }

MailerSmtpService

class MailerSmtpService{
        constructor(){
           this.smtp_con = this.smtp_service_connection()
        }        
        send (mail){
            this.smtp_con.send(mail)
            // can easily change to be this if a service requires    this implementation - smtp_con.deliver(mail)
        }        
         smtp_service_connection(){
            // Connects to smtp service
        }
    }

HtmlFormatter

class HtmlFormatter{
        constructor(){
        }        
        
        format(mail){
             // formats to html version of mail
             mail = `<html>
            <head><title>Email For You</title></head>
            <body>${mail}</body>
            </html>`;            
            return mail;
        }
    }

TextFormatter

class TextFormatter{
        constructor(){
        }        
        format(mail){
             // formats to text version of mail
             mail = "Email For You \n" + mail;          
             return mail;
        }
    }

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

Приведенный выше код делает следующее.

  • Класс, который подключается к службе smtp (MailerSmtpService).
  • Класс, который форматирует нашу почту в текст (TextFormatter).
  • Класс, который форматирует нашу почту в html (HtmlFormatter)
  • Класс, отвечающий за отправку почты (Mailer).

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

const mailer = new Mailer(“hello kwame”, [new HtmlFormatter(), new TextFormatter()])
mailer.send();

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

Принцип открытия-закрытия

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

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

Возьмем наш класс MailerSmtpService из первого примера и сделаем так, чтобы он поддерживал этот принцип.

MailerSmtpService — (Начальный)

Это наша начальная реализация для MailerSmtpService. Здесь пока ничего особенного. Все, что мы делаем, это подключаемся к службе smtp в методе-конструкторе и сохраняем результат соединения в this.smtp_con, затем мы предоставляем метод send, который принимает mail в качестве аргумента и отправляет электронное письмо.

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

class MailerSmtpService{
        constructor(){
           this.smtp_con = this.smtp_service_connection()
        }
        send (mail){
            this.smtp_con.send(mail)
            // can also be this.smtp_con.deliver(mail)
        }
        smtp_service_connection(){
            // Connects to smtp service
        }
    }

MailerSmtpService — (расширенный)

Теперь, чтобы поддержать принцип открыто-закрыто, мы удалим метод smtp_service_connection из нашего класса MailerSmtpService и скорее передадим метод в качестве параметра в конструктор MailerSmtpService, затем в подклассе (PostMarkSmtpService и SendGridSmtpService), который наследуется от MailerSmtpService, вызовем метод constructor базового класса с super(() => {}), затем мы передаем метод, который обрабатывает smtp-соединение в зависимости от используемого smtp-провайдера. Также мы переопределяем метод send в родительском классе (MailerSmtpService), и каждый из дочерних классов (PostMarkSmtpService и SendGridSmtpService) реализует собственные версии метода send.

class MailerSmtpService{
        constructor(smtp_connection = () => {
            //connects to default smtp service
        }){
           this.smtp_con = smtp_connection()
        }
        send (mail){
            this.smtp_con.send(mail)
        }
    }

PostMarkSmtpService

class PostMarkSmtpService extends MailerSmtpService {
        constructor(){
           super(() => {
                // Connects to postmark smtp service
            })
        }
        send (mail){
            this.smtp_con.send(mail)
        }
    }

сендгридсмтпсервице

class SendGridSmtpService extends MailerSmtpService {
        constructor(){
            super(() => {
                // Connects to sendgrid smtp service
            })
        }
        send (mail){
            this.smtp_con.deliver(mail)
        }
    }

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

class Mailer{
        constructor(mail, mailerFormats){
            this.mail = mail
            this.mailerFormats = mailerFormats
            this.smtpService = new PostMarkSmtpService()
            // OR this.smtpService = new SendGridSmtpService()
        }
        send(){
            // Loops through mail formats and calls the send method
            this.mailerFormats.forEach((formatter) => this.smtpService.send(formatter.format(this.mail)))
        }
    }

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

Это принцип открытого-закрытого в действии.

Лисков Принцип замещения

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

Этот принцип гласит, что

Производные или дочерние классы должны заменять свои базовые или родительские классы.

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

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

mailerSmtp: MailerSmtpService = new MailerSmtpService();
postmarkMailerSmtp: MailerSmtpService = new PostMarkSmtpService();
sendgridMailerSmtp: MailerSmtpService = new SendGridSmtpService();

Спасибо за ваше время. Подпишитесь на меня или поставьте лайк, если вам понравилась эта статья.

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