Решения на языках Ruby, Java и Julia
Завершая рассмотрение задания OOP1Creat1, приведем его решения еще
на трех языках, для которых предусмотрено создание развернутых заготовок в версии 1.1 задачника PT for OOP:
Ruby, Java и Julia. Язык Ruby имеет много общего с языком Python; это тоже язык
с динамической типизацией, в котором не требуется описывать полностью
абстрактные классы и абстрактные методы. Язык Javа имеет объектную
модель, близкую к модели языка C#. В ней объекты тоже всегда размещаются
в динамической памяти и автоматически освобождают ее благодаря наличию
сборщика мусора. Кроме того, в языке Java все методы классов по умолчанию
являются виртуальными, поэтому модификатор virtual для них указывать не
требуется (хотя можно использовать модификатор @Override, который,
подобно аналогичному модификатору для языка C++, не является
обязательным, но делает программу более наглядной). Язык Julia
имеет объектную модель, которая существенно отличается от моделей всех рассмотренных ранее языков,
что делает задачу реализации различных паттернов на данном языке особенно интересной.
Заметим, что программы, выполняющие задания на языках Ruby и Java,
тоже можно разрабатывать в среде Visual Studio Code. Кроме этой среды
задачник поддерживает среду RubyMine для языка Ruby и среду Eclipse для
языка Java.
Вначале приведем заготовку программы для языка Ruby. Сравнивая ее с
ранее приведенной заготовкой для языка Python, нетрудно заметить,
насколько близки эти языки.
[Ruby] # coding: utf-8
require "./PT"
class ConcreteProduct1
def initialize(info)
# Implement the constructor
end
def getInfo
# Implement the method
end
def transform
# Implement the method
end
end
# Implement the ConcreteProduct2 class
class Creator
def anOperation(info)
p = factoryMethod(info)
p.transform
p.transform
p.getInfo
end
end
class ConcreteCreator1 < Creator
def factoryMethod(info)
# Implement the method
end
end
# Implement the ConcreteCreator2 descendant class;
# the anOperation method should not be
# overridden in this class
def solve()
task "OOP1Creat1"
end
start_solve
Следует обратить внимание на одну особенность Ruby:
при достижения конца функции из нее возвращается последнее вычисленное значение; таким образом,
в большинстве случаев слово return можно не использовать (в отличие от языка Python,
где слово return должно обязательно указываться перед возвращаемым значением функции).
Теперь приведем решение задачи на языке Ruby.
[Ruby] # coding: utf-8
require "./PT"
class ConcreteProduct1
def initialize(info)
@info = info.downcase
end
def getInfo
@info
end
def transform
(@info.length-2).downto(0) do |i|
if @info[i] != " "
@info.insert(i+1, " ")
end
end
end
end
class ConcreteProduct2
def initialize(info)
@info = info.upcase
end
def getInfo
@info
end
def transform
(@info.length-2).downto(0) do |i|
if @info[i] != "*"
@info.insert(i+1, "**")
end
end
end
end
class Creator
def anOperation(info)
p = factoryMethod(info)
p.transform
p.transform
p.getInfo
end
end
class ConcreteCreator1 < Creator
def factoryMethod(info)
ConcreteProduct1.new(info)
end
end
class ConcreteCreator2 < Creator
def factoryMethod(info)
return ConcreteProduct2.new(info)
end
end
def solve()
task "OOP1Creat1"
c1 = ConcreteCreator1.new
c2 = ConcreteCreator2.new
5.times do
s = get
put c1.anOperation(s), c2.anOperation(s)
end
end
start_solve
Заготовка для языка Java очень похожа на заготовку для языка C#.
Основным ее отличием является то, что в данном случае дополнительные
классы описываются не внутри основного класса MyTask, содержащего метод
solve, а перед ним. Кроме того, в конце класса MyTask содержится описание
стартового метода main (это описание не требуется изменять, поэтому здесь
оно не приводится). В начале заготовки содержится директива import
java.util.ArrayList, поскольку структура данных ArrayList может оказаться
полезной при решении многих задач.
[Java] import java.util.ArrayList;
abstract class Product {
public abstract String getInfo();
public abstract void transform();
}
// Implement the ConcreteProduct1
// and ConcreteProduct2 descendant classes
abstract class Creator {
protected abstract Product factoryMethod(String info);
public String anOperation(String info) {
Product p = factoryMethod(info);
p.transform();
p.transform();
String s = p.getInfo();
return s;
}
}
// Implement the ConcreteCreator1
// and ConcreteCreator2 descendant classes;
// the anOperation method should not be
// overridden in these classes
public class MyTask extends PT
{
public static void solve() throws Exception
{
task("OOP1Creat1");
}
<Здесь содержится описание метода main>
}
Приведем реализацию классов ConcreteProduct1, ConcreteProduct2,
ConcreteCreator1, ConcreteCreator2 и метода solve. Эти реализации также
очень похожи на соответствующие реализации для языка C# (обратите
внимание на модификаторы @Override):
[Java] class ConcreteProduct1 extends Product {
private String info;
public ConcreteProduct1(String info) {
this.info = info.toLowerCase();
}
@Override
public String getInfo() {
return info;
}
@Override
public void transform() {
StringBuffer s = new StringBuffer(info);
for (int i = s.length() - 2; i >= 0; i--)
if (s.charAt(i) != ' ')
s.insert(i+1, ' ');
this.info = s.toString();
}
}
class ConcreteProduct2 extends Product {
private String info;
public ConcreteProduct2(String info) {
this.info = info.toUpperCase();
}
@Override
public String getInfo() {
return info;
}
@Override
public void transform() {
StringBuffer s = new StringBuffer(info);
for (int i = s.length() - 2; i >= 0; i--)
if (s.charAt(i) != '*')
s.insert(i+1, "**");
this.info = s.toString();
}
}
class ConcreteCreator1 extends Creator {
@Override
protected Product factoryMethod(String info) {
return new ConcreteProduct1(info);
}
}
class ConcreteCreator2 extends Creator {
@Override
protected Product factoryMethod(String info) {
return new ConcreteProduct2(info);
}
}
public static void solve() throws Exception
{
task("OOP1Creat1");
ConcreteCreator1 c1 = new ConcreteCreator1();
ConcreteCreator2 c2 = new ConcreteCreator2();
for (int i = 0; i < 5; i++) {
String info = getString();
put(c1.anOperation(info), c2.anOperation(info));
}
}
Подобно тому как в языке C# мы использовали вспомогательный класс
StringBuilder для эффективного преобразования поля info в методе
Transform, в языке Java мы для этих же целей используем класс StringBuffer,
обладающий аналогичными свойствами.
Наконец, обратимся к языку Julia. Для решения задач на этом языке с применением
электронного задачника Programming Taskbook можно использовать среду Visual Studio Code,
если установить для нее расширение для данного языка (и установить
на компьютер саму систему Julia).
Заготовка задания OOP1Creat1 для языка Julia имеет следующий вид:
[Julia] include("PT.jl")
abstract type Product
end
abstract type Creator
end
mutable struct ConcreteProduct1 <: Product
info::String
ConcreteProduct1(info::String) = new(lowercase(info))
end
function getInfo(p::ConcreteProduct1)
p.info
end
function transform!(p::ConcreteProduct1)
# Implement the function
end
struct ConcreteCreator1 <: Creator
end
function factoryMethod(::ConcreteCreator1, info::String)
ConcreteProduct1(info)
end
# Implement the ConcreteProduct2,
# ConcreteCreator2 descendant structs
# and all required functions;
# the anOperation function should not be
# overloaded for the ConcreteCreator1
# and ConcreteCreator2 structs
function anOperation(c::Creator, info::String)
p = factoryMethod(c, info)
transform!(p)
transform!(p)
getInfo(p)
end
function solve()
task("OOP1Creat1")
end
start_solve()
Поскольку язык Julia достаточно сильно отличается от «традиционных»
объектно-ориентированных языков, для него генерируются более развернутые заготовки,
наглядно демонстрирующие основные особенности данного языка.
Начнем с подробного обсуждения данной заготовки. В ее начале выполняется
подключение файла PT.jl, который содержит реализацию дополнительных функций задачника
для языка Julia, предназначенных для ввода исходных данных, вывода результатов
и отладочного вывода промежуточных данных. Затем описываются абстрактные типы Product и Creator.
Абстрактные типы не могут содержать поля (что создает дополнительные проблемы при реализации
некоторых паттернов), однако в нашем случае это не является существенным
(поскольку в данном задании не требуется включать поля в абстрактные типы).
Далее приводится описание конкретного продукта ConcreteProduct1.
Этот продукт описывается как структура struct, поскольку такая сущность, как класс (class),
в языке Julia отсутствует. Выбор названия «структура» объясняется тем, что
структуры в Julia могут содержать только поля, но не методы (единственным исключением являются
конструкторы, которые тоже можно определить непосредственно внутри структуры, как это видно
из примера описания продукта ConcreteProduct1). Вместо методов требуется описывать
обычные функции, передавая им в качестве параметра тот объект, для работы с которым предназначена данная функция.
Еще одной особенностью объектной модели Julia является полное отсутствие средств, связанных с инкапсуляцией
данных: все поля любой структуры всегда доступны из внешней программы. Кроме того, в этом модели
существенно ограничены возможности наследования, поскольку предком может быть только абстрактный тип,
а от конкретного типа нельзя порождать потомков. Для обозначения наследования используется комбинация символов <:.
Структуры в Julia по умолчанию являются неизменяемыми; это означает, что после их создания
нельзя изменить значения полей. Однако это ограничение можно отменить, описав структуру
с ключевым словом mutable. В нашем случае продукт ConcreteProduct1 описан как изменяемая структура,
поскольку при работе с ним может изменяться поле Info.
Для структуры ConcreteProduct1 уже описаны связанные с ней функции getInfo и transform!.
Следует обратить внимание на стандартный способ именования функций в Julia (имя начинается с маленькой буквы,
а дополнительные слова, входящие в имя, начинаются с заглавной буквы), а также на то, что имена
функций, изменяющих свои параметры, рекомендуется завершать восклицательным знаком.
В языке Julia, как и в языке Ruby, при достижении конца функции возвращается последнее вычисленное в ней значение.
Для создания объекта следует (как и в языках Python и Ruby) вызвать его конструктор, указав имя структуры
и список параметров. Заметим, что для любой структуры сразу доступен конструктор со списком параметров,
совпадающим с набором полей структуры (который создает объект и копирует параметры в соответствующие поля).
Однако в структуре ConcreteProduct1 такой конструктор переопределен, чтобы в поле info записывалась
строка, преобразованная функцией lowercase в нижнему регистру букв (и для этого используется особый
внутренний конструктор new, который можно вызывать только внутри определения структуры).
Для завершения реализации структуры ConcreteProduct1 нам достаточно реализовать функцию transform!.
Это можно сделать следующим образом, используя вспомогательную функцию transf:
[Julia] function transf(s::String, subs::String, c::Char)
res = ""
for e in s
res *= e
if e != c
res *= subs
end
end
res[1:length(res) - length(subs)]
end
function transform!(p::ConcreteProduct1)
p.info = transf(p.info, " ", ' ')
end
В данном варианте алгоритма (который можно реализовать и для ранее рассмотренных языков программирования)
мы просто формируем требуемую строку, включая в нее символы e исходной строки и дополнительные подстроки subs,
после чего удаляем «лишнюю» подстроку, добавленную после последнего символа исходной строки.
Данный пример показывает, что для сцепления строк в языке Julia используется операция * (а не +, как в большинстве
других языков), а также что при работе со строками можно использовать срезы. Однако синтаксис срезов в Julia имеет важные
отличия от синтаксиса языка Python: индексирование выполняется от 1 (а не от 0), индекс, указанный последним,
включается в срез, а если требуется указать шаг, отличный от 1, то он указывается между первым и последним индексом: [first:step:last].
Заметим, что от 1 индексируются все коллекции Julia, что необходимо принимать во внимание при решении задач, в которых
в качестве исходных данных даются некоторые индексы, поскольку при генерации исходных данных предполагается, что индексы начинаются от 0.
Имея в качестве образца структуры ConcreteProduct1 и ConcreteCreator1, а также связанные с ними функции, включая
вспомогательную функцию transf, мы можем легко реализовать аналогичным образом структуры ConcreteProduct2 и ConcreteCreator2:
[Julia] mutable struct ConcreteProduct2 <: Product
info::String
ConcreteProduct2(info::String) = new(uppercase(info))
end
function getInfo(p::ConcreteProduct2)
p.info
end
function transform!(p::ConcreteProduct2)
p.info = transf(p.info, "**", '*')
end
struct ConcreteCreator2 <: Creator
end
function factoryMethod(::ConcreteCreator2, info::String)
ConcreteProduct2(info)
end
Осталось протестировать созданные структуры и функции, выполнив требуемые действия
по вводу исходных данных, их обработке и выводу результатов в функции solve.
Заметим, что для ввода и вывода данных в программах на языке Julia
в задачнике предусмотрены функции с теми же именами (и тем же поведением), что и для языка Python:
[Julia] function solve()
task("OOP1Creat1")
c1 = ConcreteCreator1()
c2 = ConcreteCreator2()
for i in 1:5
info = get()
put(anOperation(c1, info))
put(anOperation(c2, info))
end
end
Запустив данную программу, мы получим сообщение о том, что задание выполнено.
Следует подчеркнуть, что правильная работы программы обеспечивается благодаря
тому, что при вызове функции anOperation анализируется фактический тип структуры c1,
что позволяет вызывать для нее правильный вариант фабричного метода и создавать
требуемый продукт. Аналогичный анализ выполняется и при выборе варианта функции transform!,
вызываемой внутри функции anOperation. Интересно отметить, что с помощью аналогичного
механизма динамического определения типа параметров функций мы можем упростить
наше решение, заменив реализации функции getInfo для конкретных продуктов
ConcreteProduct1 и ConcreteProduct2 единственной реализацией для их общего абстрактного предка Product:
[Julia] function getInfo(p::Product)
p.info
end
На первый взгляд подобная реализация является неверной, поскольку абстрактный тип Product
не имеет поля info. Однако такое описание параметра просто означает, что эта функция может вызываться
для всех конкретных структур, порожденных от типа Product, и поскольку обе эти конкретные структуры
имеют поле info, функция getInfo будет работать для них правильно.
Данный пример демонстрирует роль абстрактных классов (и концепции наследования от них) в языке Julia.
Важность этой роли будет проявляться при реализации практически всех паттернов проектирования на этом языке.
|