Шаблоны проектирования с точки зрения Scala

В этой статье я кратко объясню шаблон проектирования компоновщика, используя несколько примеров кода. Это будет сделано с точки зрения Scala, чисто объектно-ориентированного языка. Фрагменты кода можно найти на моем github.

Шаблоны проектирования

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

Шаблоны проектирования в Scala

Scala — это богатый выразительный язык, и он делает некоторые шаблоны проектирования ненужными или более простыми. Кроме того, благодаря своим функционально-ориентированным возможностям он сделает доступными дополнительные шаблоны проектирования, которых нет в других традиционных объектно-ориентированных языках программирования.

Творческие шаблоны

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

Шаблон строителя

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

BookBuilder с помощью своих методов set сохраняет атрибуты title, author и price и возвращает свой собственный экземпляр, так что эти методы могут быть объединены в цепочку. После вызова метода сборки в классе BookBuilder он создается с атрибутами, ранее сохраненными в BookBuilder. Чтобы клиент не мог напрямую изменять атрибуты BookBuilder, а мог изменять их только с помощью различных методов набора, атрибуты этого класса являются закрытыми. Также в случае класса Book атрибуты являются закрытыми, что позволяет клиенту изменять их только косвенно через метод сборки BookBuilder. Ниже приведена реализация кода диаграммы.

class Book(val title: String, val author: String, val price: Double) {
  def getTitle(): String = this.title
  def getAuthor(): String = this.author
  def getPrice(): Double = this.price
}

class BookBuilder {
  private var title = ""
  private var author = ""
  private var price = 0.0

  def setTitle(title: String): BookBuilder = {
    this.title = title
    this
  }

  def setAuthor(author: String): BookBuilder = {
    this.author = author
    this
  }

  def setPrice(price: Double): BookBuilder = {
    this.price = price
    this
  }

  def build(): Book = new Book(this.title, this.author, this.price)
}

object Test extends App {
  val book: Book = new BookBuilder()
    .setTitle("The Hobbit")
    .setAuthor("J.R.R. Tolkien")
    .setPrice(10.5)
    .build()

  System.out.println(s"The book ${book.getTitle()} written by ${book.getAuthor()} got a price of ${book.getPrice()} euros")
}

BookBuilder имеет значения по умолчанию для своих атрибутов. Атрибуты BookBuilder используются в качестве параметров для создания экземпляра класса Book с помощью метода сборки, поэтому, если для BookBuilder не предоставлены значения, экземпляр класса Book создается со значениями по умолчанию для BookBuilder. Когда разные конструкторы, каждый с различным подмножеством атрибутов, должны быть доступны или расширены, с помощью этого шаблона потребуется только реализовать метод set и get в BookBuilder для каждого из новых атрибутов, также со значением по умолчанию, и эти атрибуты нужно будет добавить в конструктор Book. Таким образом, с помощью этого шаблона получается более расширяемый код с меньшим количеством шаблонов. Некоторые разработчики предпочитают иметь значение None для значения атрибутов по умолчанию или использовать Option. Выбор будет зависеть от решаемой проблемы и руководств по стилю, используемых командой.

Тем не менее, несмотря на то, что это уменьшает шаблон, который имеет конструктор каждого подмножества атрибутов, шаблон все еще остается, поскольку необходимо реализовать методы set и get. Более того, у BookBuilder есть проблема, связанная с его изменяемостью, что усложняет параллельное выполнение, и одна из основных задач Scala — создание масштабируемого кода. В Scala есть некоторые функции для предоставления неизменяемых классов и сокращения шаблонов, например класс case. Класс case Scala предоставляет сразу несколько методов, основанных на значениях, переданных его основному конструктору, что сокращает время и количество ошибок при разработке этих методов. Предоставляемые методы: equals, hashCode, toString и методы доступа к атрибутам. Далее следует реализация шаблона с использованием класса case.

case class Book(title: String = "", author: String = "", price: Double = 0.0)

object Test extends App {
  val book: Book = new Book("The Hobbit", "J.R.R. Tolkien", 10.5)
  System.out.println(s"The book ${book.title} written by ${book.author} got a price of ${book.price} euros")
}

Как можно заметить, получается более расширяемый код с меньшим количеством шаблонов. Также книга неизменна. Экземпляр создается со значениями по умолчанию или значениями, предоставленными его конструктору, поэтому создается только один экземпляр, никакой другой промежуточной версии объекта, в котором он создан, как в случае BookBuilder, который хранит значения по умолчанию и более поздние промежуточные результаты. с помощью его заданных методов.

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

case class Book(title: String = "", author: String = "", price: Double = 0.0) {
  require(title != "", "A title it's required for the book")
  require(author != "", "An author it's required for the book")
}

object Test extends App {
  val book: Book = new Book("The Hobbit", "J.R.R. Tolkien", 10.5)
  System.out.println(s"The book ${book.title} written by ${book.author} got a price of ${book.price} euros")
  val anonymousBook: Book = new Book(title = "Book X", price = 10.5)
  System.out.println(s"The book ${anonymousBook.title} written by ${anonymousBook.author} got a price of ${anonymousBook.price} euros")
}

Когда предикаты внутри операторов require не оцениваются как истинные, java.lang.Illegal ArugmentExcepction выдается во время выполнения с соответствующим сообщением о неправильном операторе.

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

Чтобы BookBuilder мог оценить, удовлетворяет ли создаваемая книга проверке, реализованы некоторые типы, HasTitle и HasAuthor. Оба наследуются от BookConstraint, который является типом, используемым в качестве универсального типа в BookBuilder. Эти ограничения объявлены как отложенные, чтобы их нельзя было расширить за пределы исходного файла, что предотвращает их неправильное использование. Далее идет их реализация.

sealed trait BookConstraint
sealed trait HasTitle extends BookConstraint
sealed trait HasAuthor extends BookConstraint

BookBuilder использует универсальный тип, расширяющий BookConstraint. Далее идет подпись класса.

class BookBuilder[SatisfiedConstraint <: BookConstraint] protected (val title: String, val author: String, val price: Double)

BookBuilder имеет два конструктора. Оба защищены, чтобы BookBuilder мог быть создан в сопутствующем объекте, который у него есть, или создан внутри класса BookBuilder. Первый конструктор не принимает аргументов и задает некоторые значения по умолчанию для BookBuilder. Это конструктор, который используется сопутствующим объектом. Второй конструктор определен в сигнатуре класса и принимает параметры, предоставленные для создания экземпляра BookBuilder. Этот второй конструктор используется различными методами набора BookBuilder для создания экземпляра нового объекта BookBuilder с полем, предоставленным методу набора, и другими полями, уже сохраненными в BookBuilder. Далее идет реализация конструктора вне сигнатуры.

protected def this() = this("", "", 0.0)

Метод setTitle возвращает BookBuilder с HasTitle для его универсального типа. setAuthor проверяет, что универсальный тип BookBuilder является типом HasTitle, и возвращает BookBuilder с типом HasAuthor. Наконец, метод setPrice возвращает новый BookBuilder с типом, полученным как общий тип. Далее идет реализация методов.

def setTitle(title: String): BookBuilder[HasTitle] = {
  new BookBuilder[HasTitle](title, this.author, this.price)
}

def setAuthor(author: String)(implicit ev: SatisfiedConstraint =:= HasTitle): BookBuilder[HasAuthor] = {
  new BookBuilder[HasAuthor](this.title, author, this.price)
}

def setPrice(price: Double): BookBuilder[SatisfiedConstraint] = {
  new BookBuilder[SatisfiedConstraint](this.title, this.author, price)
}

BookBuilder также имеет метод сборки, который после проверки того, что BookBuilder имеет общий тип HasAuthor, создает экземпляр книги с атрибутами из BookBuilder. Далее идет реализация класса Book и метода сборки из BookBuilder.

class Book(val title: String, val author: String, val price: Double)
def build()(implicit ev: SatisfiedConstraint =:= HasAuthor): Book = new Book(this.title, this.author, this.price)

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

object PersonBuilder {
  def apply() = new PersonBuilder
}

У него есть метод применения, который является синтаксическим сахаром и делает доступным для клиента вызов метода применения вместо использования PersonBuilder.apply(), используя PersonBuilder(). Этот подход необязателен и считается опасным. Тем не менее, если сборщик хорошо документирован, проблем быть не должно, и использование такого подхода заставляет PersonBuilder ощущаться как поддержка нативного языка, и это одна из основных целей Scala — разрабатывать библиотеки, которые чувствуют, что у них есть поддержка нативного языка для клиента. .

Ниже приведен пример реализации клиента с помощью BookBuilder.

object Test extends App {
  val bookHobbit: Book = BookBuilder()
    .setTitle("The Hobbit")
    .setAuthor("J.R.R. Tolkien")
    .setPrice(10.5)
    .build()

  System.out.println(s"The book ${bookHobbit.title} written by ${bookHobbit.author} got a price of ${bookHobbit.price} euros")

  val bookNarnia: Book = BookBuilder()
    .setPrice(12.0)
    .setTitle("The Chronicles of Narnia")
    .setAuthor("C. S. Lewis")
    .build()

  System.out.println(s"The book ${bookNarnia.title} written by ${bookNarnia.author} got a price of ${bookNarnia.price} euros")
}

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

Понравился контент?

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

Хотите подключиться?

Linkedin, Medium, GitHub

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу