Вступление
В этой статье мы разберем шаблон проектирования Builder и покажем его применение на Java.
Шаблоны проектирования - это просто набор стандартизированных практик, обычно используемых в индустрии разработки программного обеспечения. Они представляют собой предлагаемые сообществом решения общих проблем, с которыми сталкиваются повседневные задачи, связанные с разработкой программного обеспечения.
Знание абстракции, наследования и полиморфизма не обязательно сделает вас хорошим объектно-ориентированным дизайнером из коробки. Эксперт по дизайну создает дизайн, который удобен в обслуживании и гибок, но, что самое главное, понятен.
Хорошая идея, связанная с изобретателем, - не такая уж хорошая идея.
Шаблоны творческого дизайна
Шаблоны творческого дизайна сосредоточены на создании объектов. Создание объектов - действительно важная часть объектно-ориентированного проектирования, и оптимизация этой задачи в высокопроизводительных и сложных приложениях имеет первостепенное значение.
Эти шаблоны контролируют то, как мы определяем и проектируем объекты, а также то, как мы создаем их экземпляры. Некоторые инкапсулируют логику создания вдали от пользователей и обрабатывают создание ( Factory и Abstract Factory ), некоторые сосредотачиваются на процессе построения самих объектов ( Builder ), некоторые минимизируют стоимость создания ( Prototype ), а некоторые контролируют количество экземпляров на вся JVM ( синглтон ).
В этой статье мы углубимся в шаблон проектирования Builder .
Шаблон проектирования Builder
Определение
Шаблон проектирования Builder отделяет построение сложного объекта от
его представления. Это делается с помощью вложенного static
класса,
который присваивает требуемые значения перед возвратом экземпляра.
Также следует отметить, что шаблон Builder часто используется для создания неизменяемых объектов. Существование методов установки в значительной степени противоречит неизменности, и поскольку мы не используем их, когда у нас есть шаблон Builder, гораздо проще создавать неизменяемые объекты - без необходимости передавать все параметры в вызове конструктора.
Мотивация
Создать экземпляр объекта на Java просто. Мы используем new
, за
которым следуют конструктор и параметры, которые мы назначаем объекту.
Типичный экземпляр может выглядеть так:
Cookie chocolateChip = new Cookie("Chocolate Chip Cookie");
Конструктору передается String, и без определения класса становится очевидным, что он представляет тип / имя cookie.
Хотя, если мы хотим создать экземпляр более сложного класса, такого как нейронная сеть, в этом стиле, мы сталкиваемся с:
SingleLayerNetwork configuration = new NeuralNetConfiguration(4256, STOCHASTIC_GRADIENT_DESCENT,
new Adam(), 1e-4, numRows*numColumns,
1000, RELU, XAVIER);
Даже с 8 параметрами код быстро становится нечитаемым и непонятным. Даже для разработчика, который в первую очередь написал определение класса. Что происходит, когда новый разработчик пытается использовать этот класс?
Или, еще лучше, представьте, что вам нужно вызвать конструктор этого класса, чтобы создать его экземпляр:
public class SmartHome {
private String name;
private int serialNumber;
private String addressName;
private String addressNumber;
private String city;
private String country;
private String postalCode;
private int light1PortNum;
private int light2PortNum;
private int door1PortNum;
private int door2PortNum;
private int microwavePortNum;
private int tvPortNum;
private int waterHeaterPortNum;
public SmartHome(String name, int serialNumber, String addressName, String addressNumber, String city, String country, String postalCode, int light1PortNum, int light2PortNum, int door1PortNum, int door2PortNum, int microwavePortNum, int tvPortNum, int waterHeaterPortNum) {
// Assigning values in the constructor call
}
// Getters and Setters
}
Мы сталкиваемся со слишком большим количеством аргументов конструктора, и при небольшом разнообразии типов мы будем смотреть на вызов огромного конструктора, не зная, что к чему.
Также обратите внимание, что два конструктора с одним и тем же типом параметра, но с разными именами переменных не принимаются в Java.
Наличие этих двух конструкторов недопустимо в Java, поскольку компилятор не может их различить:
public SmartHome(int door1PortNum) { ... }
public SmartHome(int door2PortNum) { ... }
Даже если у нас есть один конструктор с типом параметра int
:
public SmartHome(int portNum) { ... }
Мы знаем, что нам нужно установить номер порта, но мы не узнаем, является ли этот номер портом для двери, света, микроволновой печи, телевизора или водонагревателя.
Этот класс быстро становится непригодным для использования в коллективной среде. Даже если вы представляете одного человека, удачи вам в запоминании порядка параметров после недели, когда вы не создавали экземпляр класса.
Вот где вступает в силу паттерн Строителя:
Шаблон Builder отделяет конструкцию от представления.
Что это значит?
Конструкция выполняется в самом классе. Представление - это то, что мы видим как пользователь класса. Прямо сейчас оба приведенных выше класса связаны между собой - мы напрямую вызываем конструктор с переданными аргументами.
Разделив эти два, мы можем сделать представление класса намного проще, аккуратнее и читабельнее, в то время как конструктор выполняет свою часть.
Выполнение
Есть несколько шагов, которые нужно предпринять, чтобы реализовать
паттерн Строитель. Продолжая наши предыдущие примеры, мы будем
использовать SmartHome
чтобы показать эти шаги:
static
строителя должен быть вложен в наш классSmartHome
- Конструктор
SmartHome
должен бытьprivate
чтобы конечный пользователь не мог его вызвать. - Класс построителя должен иметь интуитивно понятное имя, например
SmartHomeBuilder
- Класс
SmartHomeBuilder
будет иметь те же поля, что и классSmartHome
- Поля в
SmartHome
могут бытьfinal
или нет, в зависимости от того, хотите ли вы, чтобы они были неизменными или нет. - Класс
SmartHomeBuilder
будет содержать методы, устанавливающие значения, аналогичные методам установки. Эти методы будут использоватьSmartHomeBuilder
в качестве возвращаемого типа, назначать переданные значения полям статического класса построителя и следовать соглашению об именах построителя. Они обычно начинаются сwith
,in
,at
и т.д. вместоset
. - Класс статического построителя будет содержать
build()
который вводит эти значения вSmartHome
и возвращает его экземпляр.
С учетом сказанного, давайте реализуем шаблон Builder в нашем примере класса:
public class SmartHome {
// Fields omitted for brevity
// The same fields should be in `SmartHome` and `SmartHomeBuilder`
// Private constructor means we can't instantiate it
// by simply calling `new SmartHome()`
private SmartHome() {}
public static class SmartHomeBuilder {
private String name;
private int serialNumber;
private String addressName;
private String addressNumber;
private String city;
private String country;
private String postalCode;
private int light1PortNum;
private int light2PortNum;
private int door1PortNum;
private int door2PortNum;
private int microwavePortNum;
private int tvPortNum;
private int waterHeaterPortNum;
public SmartHomeBuilder withName(String name) {
this.name = name;
return this;
}
public SmartHomeBuilder withSerialNumber(int serialNumber) {
this.serialNumber = serialNumber;
return this;
}
public SmartHomeBuilder withAddressName(String addressName) {
this.addressName = addressName;
return this;
}
public SmartHomeBuilder inCity(String city) {
this.city = city;
return this;
}
public SmartHomeBuilder inCountry(String country) {
this.country = country;
return this;
}
// The rest of the methods are omitted for brevity
// All follow the same principle
public SmartHome build() {
SmartHome smartHome = new SmartHome();
smartHome.name = this.name;
smartHome.serialNumber = this.serialNumber;
smartHome.addressName = this.addressName;
smartHome.city = this.city;
smartHome.country = this.country;
smartHome.postalCode = this.postalCode;
smartHome.light1PortNum = this.light1PortNum;
smartHome.light2PortNum = this.light2PortNum;
smartHome.door1PortNum = this.door1PortNum;
smartHome.door2PortNum = this.door2PortNum;
smartHome.microwavePortNum = this.microwavePortNum;
smartHome.tvPortNum = this.tvPortNum;
smartHome.waterHeaterPortNum = this.waterHeaterPortNum;
return smartHome;
}
}
}
У SmartHome
нет общедоступных конструкторов, и единственный способ
создать SmartHome
- использовать SmartHomeBuilder
, например:
SmartHome smartHomeSystem = new SmartHome
.SmartHomeBuilder()
.withName("RaspberrySmartHomeSystem")
.withSerialNumber(3627)
.withAddressName("Main Street")
.withAddressNumber("14a")
.inCity("Kumanovo")
.inCountry("Macedonia")
.withPostalCode("1300")
.withDoor1PortNum(342)
.withDoor2PortNum(343)
.withLight1PortNum(211)
.withLight2PortNum(212)
.withMicrowavePortNum(11)
.withTvPortNum(12)
.withWaterHeaterPortNum(13)
.build();
System.out.println(smartHomeSystem);
Хотя мы усложнили сам класс, включив вложенный класс с повторяющимися полями, представление отделено от создания.
Очевидно, что мы создаем при создании экземпляра объекта. Он читабелен, понятен, и любой может использовать ваши классы для создания объектов.
Возвращаясь к примеру нейронной сети из реального мира, это выглядело бы примерно так:
MultiLayerNetwork conf = new NeuralNetConfiguration.Builder()
.seed(rngSeed)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam())
.l2(1e-4)
.list()
.layer(new DenseLayer.Builder()
.nIn(numRows * numColumns) // Number of input datapoints.
.nOut(1000) // Number of output datapoints.
.activation(Activation.RELU) // Activation function.
.weightInit(WeightInit.XAVIER) // Weight initialization.
.build())
.layer(new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
.nIn(1000)
.nOut(outputNum)
.activation(Activation.SOFTMAX)
.weightInit(WeightInit.XAVIER)
.build())
.pretrain(false).backprop(true)
.build()
[Кредит кода: DeepLearning4j - QuickStart]{.small}
Плюсы и минусы
Помимо наиболее очевидного момента использования паттерна Строитель, есть пара других преимуществ, которые на первый взгляд могут быть не слишком очевидными:
- Вы можете изменить реализацию объекта так, как хотите, и просто обновить методы. Конечный пользователь сталкивается с абстрактным интерфейсом через класс статического построителя и не заботится о базовой реализации.
- Он поддерживает инкапсуляцию, отделяя представление объекта от конструкции.
Единственный реальный недостаток - это увеличение количества кода в моделях предметной области. Обычно они уже длинные, хотя и относительно простые (поля, геттеры и сеттеры). Хотя в любом случае вы редко будете вмешиваться в эти классы.
В общем, плюсы значительно перевешивают недостатки, когда дело доходит до шаблона Builder, поэтому он обычно используется во многих , особенно сложных приложениях, фреймворках и библиотеках.
Заключение
Шаблоны проектирования - это просто набор стандартизированных практик, используемых в индустрии разработки программного обеспечения. Они представляют собой предлагаемые сообществом решения общих проблем, с которыми сталкиваются повседневные задачи, связанные с разработкой программного обеспечения.
В этой статье мы углубились в ключевой шаблон творческого проектирования, который заботится о создании объектов и позволяет разработчикам создавать сложные объекты с гораздо меньшим количеством ошибок, вызванных человеком, а также улучшает ремонтопригодность и масштабируемость.
Шаблон Builder Design предлагает несколько преимуществ по сравнению с простым созданием экземпляров классов с помощью конструкторов, с минусом, который на самом деле не сравнится с количеством преимуществ, которые вы можете получить от его использования.