УДК 004.053
М.Н. Гончарова
ПРИМЕНЕНИЕ ОСНОВНЫХ ШАБЛОНОВ ПРОЕКТИРОВАНИЯ В ОБЪЕКТНО-ФУНКЦИОНАЛЬНЫХ ЯЗЫКАХ
Шаблоны проектирования являются одним из ключевых инструментов в разработке програмного обеспечения и позволяют не только сэкономить время, затраченное на создание продукта, но и значительно улучшить его качество. Классические шаблоны являются объектно-ориентированными, но даже в функциональных языках они не теряют своей актуальности.
Ключевые слова: шаблоны проектирования, java, scala, объектно-ориентированный подход, функциональный подход, гибкость, расширяемость, повторное использование.
Шаблоны проектирования - это эффективные способы решения для часто встречающихся задач, в частности при проектировании и разработке программного обеспечения. Шаблон не является законченным для применения образцом кода, а лишь описывает реализацию задачи для применения в различных ситуациях и удовлетворения конкретным требованиям. Значительное ускорение процесса разработки, улучшение качества и читаемости кода, унификация использующейся терминологии - все эти преимущества влечёт за собой использование шаблонов проектирования.
Объектно-ориентированный подход в программировании обеспечивает гибкость и лучшую возможность повторного использования кода в сравнении с функциональным и поэтому классическими шаблонами проектирования являются именно объектно-ориентированные. Они описывают возможные взаимодействия и отношения, связывающие между собой объекты и классы, к которым они принадлежат.
Довольно часто в случае отсутствия в языке программирования нужных конструкций, их с успехом заменяют шаблоны проектирования. Scala имеет выразительный синтаксис, который выглядит весьма свежо после приевшихся шаблонов Java. Он поддерживает функциональный стиль программирования, не отказываясь при этом от объектно-ориентированного стиля, давая возможность осваивать новые парадигмы постепенно и позволяет реализовать большую часть элементарных шаблонов проектирования с использованием синтаксических конструкций [1].
Рассмотрим реализации нескольких шаблонов проектирования для наглядной демонстрации соотношения таких языков, как Java и Scala.
Фабричный метод относится к порождающим шаблонам проектирования. Он предоставляет интерфейс для создания экземпляров некоторого класса. Данный шаблон позволяет создавать объекты разных типов, оставляя систему независимой непосредственно от самого процесса создания и от типов создаваемых экземпляров.
Для создания нового экземпляра класса с помощью конструктора в Java применяется оператор new. Для применения паттерна мы используем специальный метод для создания объекта.
© Гончарова М.Н., 2014.
public interface Animal {} public class Dog implements Animal {} public class Cat implements Animal {} public class AnimalFactory {
public static Animal createAnimal(String kind) { if ("cat".equals(kind)) { return new Cat();
}
if ("dog".equals(kind)) { return new Dog();
}
throw new IllegalArgumentException();
}
}
Animal animal = AnimalFactory.createAnimal("dog");
Scala, помимо конструкторов, позволяет использовать особую синтаксическую конструкцию, которая является удобным фабричным методом.
trait Animal
private class Dog extends Animal private class Cat extends Animal object Animal {
def apply(kind: String) = kind match { case "dog" => new Dog() case "cat" => new Cat()
}
}
Animal("dog")
Мы определили фабричный метод в синглтоне с таким же именем и в том же исходном файле. Невозможность делегирования создания объекта подклассам ограничивает такой синтаксис и делает его статической реализацией шаблона.
Следующим для рассмотрения порождающим шаблоном для проектирования будет отложенная инициализация, являющаяся частным случаем отложенных вычислений. Отложенные вычисления позволяют сократить общий объем вычислений, так как производятся непосредственно перед первым использованием. Такой подход позволяет отложить или вовсе избежать дорогостоящих операций.
Реализация данного шаблона на Java предполагает использование null для обозначения неинициализированного состояния. Но что делать в том случае, если null является допустимым значением при инициализации? В таком случае нам необходимо использовать дополнительный флаг для обозначения состояния инициализации и синхронизировать доступ к нему во избежание состояния гонки при многопоточном исполнении. Для повышения эффективности синхронизации может использоваться блокировка с двойной проверкой, что еще более усложнит код.
private volatile Component component;
public Component getComponent() { Component result = component; if (result == null) {
synchronized(this) {
result = component; if (result == null) {
component = result = new Component();
}
}
}
return result;
}
Component component = getComponent();
Пример кода с использованием встроенного механизма Scala.
lazy val x = {
print("(computing x) ") 21
}
print("x = ") println(x)
// x = (computing x) 21
Отложенная инициализация прекрасно справляется с null в значениях и предоставляет безопасный многопоточный доступ.
Шаблон синглтон предоставляет возможность ограничения количества объектов класса, а если точнее, то он гарантирует существование только одного экземпляра и предоставляет единую точку доступа к нему.
Данный шаблон используется для организации доступа к глобальной реализации некоторого интерфейса, например пула соединений с базой данной, логирования, объектов драйверов и т.д.
public class Cat implements Runnable {
private static final Cat instance = new Cat();
private Cat() {} public void run() { // do nothing
}
public static Cat getInstance() { return instance;
}
}
Cat.getInstance().run()
Лаконичный вариант реализации в Scala
object Cat extends Runnable {
def run() {
// do nothing
}
}
Cat.run()
Шаблон декоратор используется для добавления нового функционала, не затрагивающего другие объекты того же класса и является хорошей альтернативой в тех случаях, когда использование наследования невозможно в силу статичности решения и распространения его на весь класс целиком.
Для того, чтобы реализовать шаблон декоратор на Java, необходимо определить новый класс, который реализует общий интерфейс и является базовым декоратором, а затем создать конкретные декораторы, используя наследование от базового. При необходимости можно получать цепочки декораторов, передавая их друг в друга и добавляя новые возможности объекту.
public interface OutputStream { void write(byte b); void write(byte[] b);
}
public class FileOutputStream implements OutputStream { }
public abstract class OutputStreamDecorator implements OutputStream {
protected final OutputStream delegate;
protected OutputStreamDecorator(OutputStream delegate) { this.delegate = delegate;
}
public void write(byte b) { delegate.write(b); } public void write(byte[] b) { delegate.write(b); }
}
public class BufferedOutputStream extends OutputStreamDecorator { public BufferedOutputStream(OutputStream delegate) { super(delegate);
}
public void write(byte b) { // ...
delegate.write(buffer)
}
}
new BufferedOutputStream(new FileOutputStream("foo.txt"));
В Scala существует возможность переопределения интерфейса без указания конкретной реализации.
trait OutputStream { def write(b: Byte) def write(b: Array[Byte])
}
class FileOutputStream(path: String) extends OutputStream { } trait Buffering extends OutputStream { abstract override def write(b: Byte) { // ...
super.write(buffer)
}
}
new FileOutputStreamCfoo.txt") with Buffering
Null объект - объект, представляющий определенное нейтральное поведение. Объектно-ориентированные языки разрешают использование null в качестве валидного значения объекта, но ссылки на такие объекты требуют проверки перед использованием, так как их методы в большинстве случаев не могут быть вызваны. Использование шаблона null объекта позволяет избежать таких проверок.
В Java для реализации шаблона создаётся специальный подкласс с методами без реализации.
public interface Sound { void play();
}
public class Music implements Sound { public void play() {}
}
public class NullSound implements Sound { public void play() {}
}
public class SoundSource {
public static Sound getSound() { return available ? music : new NullSound();
}
}
SoundSource.getSound().play();
В Scala использование предопределенного типа Option обеспечивает схожий подход.
trait Sound { def play()
}
class Music extends Sound { def playf) {}
}
object SoundSource {
def getSound: Option[Sound] =
if (available) Some(music) else None
}
for (sound <- SoundSource.getSound) { sound.play()
}
Шаблон стратегия определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются [2]. Этот шаблон удобно использовать, если возникает необходимость смены алгоритма во время работы приложения.
Обычная реализация в Java представляет собой иерархию классов, наследующих основной интерфейс.
public interface Strategy {
int computefint a, int b);
}
public class Add implements Strategy {
public int computefint a, int b) { return a + b; }
}
public class Multiply implements Strategy {
public int computefint a, int b) { return a * b; }
}
public class Context {
private final Strategy strategy;
public ContextfStrategy strategy) { this.strategy = strategy; } public void usefint a, int b) { strategy.computefa, b); }
}
new Contextfnew Multiply()).use(2, 3);
В Scala функции - это конструкции первого класса, поэтому шаблон можно реализовать используя только средства самого языка.
type Strategy = (Int, Int) => Int class Context(computer: Strategy) {
def use(a: Int, b: Int) { computer(a, b) }} val add: Strategy = _ + _ val multiply: Strategy = _ * _ new Context(multiply).use(2, 3)
Шаблон Команда инкапсулирует запрос в виде объекта, делая возможной параметризацию клиентских объектов с другими запросами, а также поддержку отмены операций [3]. Для реализации этого шаблона в Java вызов обертывается в объект.
public class PrintCommand implements Runnable { private final String s;
PrintCommand(String s) { this.s = s; } public void run() {
System.out.println(s);
}
}
public class Invoker {
private final List<Runnable> history = new ArrayList<>(); void invoke(Runnable command) { command.run(); history.add(command);
}
}
Invoker invoker = new Invoker(); invoker.invoke(new PrintCommand("foo")); invoker.invoke(new PrintCommand("bar"));
В Scala же для этих целей используется рассмотренный ранее механизм отложенных вычислений.
object Invoker {
private var history: Seq[() => Unit] = Seq.empty
def invoke(command: => Unit) { // by-name parameter command
history :+= command _
}
}
Invoker.invoke(println("foo")) Invoker.invoke { println("bar 1") println("bar 2")
}
Цепочка ответственности - поведенческий шаблон, который выстраивает объекты составных элементов приложения, которые связаны между собой, по цепочке для передачи запроса от отправителя получателю и позволяет обработать запрос нескольким объектам. Классическая реализация шаблона предусматривает реализацию базового интерфейса каждым объектом, входящим в цепочку, и содержание ссылки на следующий объект для обработки.
public abstract class EventHandler { private EventHandler next;
void setNext(EventHandler handler) { next = handler; } public void handle(Event event) {
if (canHandle(event)) doHandle(event); else if (next != null) next.handle(event);
}
abstract protected boolean canHandle(Event event); abstract protected void doHandle(Event event);
}
public class KeyboardHandler extends EventHandler { protected boolean canHandle(Event event) {
return "keyboard".equals(event.getSource());
}
protected void doHandle(Event event) { }} KeyboardHandler handler = new KeyboardHandler(); handler.setNext(new MouseHandler());
Решение данной проблемы в Scala подразумевает более элегантный подход с использованием частичного вызова функций [4].
case class Event(source: String)
type EventHandler = PartialFunction[Event, Unit] val defaultHandler: EventHandler = PartialFunction(_ => ()) val keyboardHandler: EventHandler = { case Event("keyboard") => /* ... */
}
def mouseHandler(delay: Int): EventHandler = { case Event("mouse") => /* ... */
}
keyboardHandler.orElse(mouseHandler(100)).orElse(defaultHandler)
Библиографический список
1. Хорстман К. Функциональное программирование. Scala для нетерпеливых. ДМК Пресс, 2013.
2. http://sergeyteplyakov.blogspot. com/2014/02/singleton-pattern. html
3. Эрик Фримен, Элизабет Фримен, Кэтти Сьерра, Берт Бейтс. Паттерны проектирования. Питер,
2011.
4. http://twitter.github.io/effectivescala/index-ru.html#Функциональное программирование-Частичные функции
ГОНЧАРОВА Мария Николаевна - магистрант факультета автоматизации и информационных технологий, Центрально-Азиатский университет (Казахстан).