Конкурс окончен, смотрите результаты в конце вопроса.
Я думаю, многие из вас видели вопросы, состоящие из просьбы перевода программы с одного языка на другой. Давайте-ка покажем, как делать такие вещи правильно! В нашем конкурсе мы исходим из такой простой программы на Паскале:
program test;
var a, b, c: integer;
begin
readln(a);
readln(b);
c := a + b;
writeln(c);
end.
(Она же на ideone.)
Задание состоит в следующем: вы должны перевести программу на любой-язык так, чтобы сохранить как можно больше от исходного текста программы. В качестве целевого языка, понятно, исключаются языки группы Паскаля (Delphi, Algol, Oberon, Modula, etc., все языки, в которых используется begin/end для группировки команд в составную команду).
Вы можете дописывать конструкции до и после данного в условии текста, но не внутри его (точнее, можете и внутри, но это будет считаться изменением — смотрите ниже условия подсчёта). Сам текст желательно менять как можно меньше.
Ограничения: Строки исходной программы между begin и end должны сохранять свой смысл. Они должны выполняться, и при их выполнении должно происходить в общих чертах то же, что и в исходной программе: readln должно считывать значения с консоли, c := a + b должно складывать значения двух переменных (или что там есть в вашем языке) в третью, writeln должно выводить правильный результат на консоль. (Это означает, что вы не можете просто закомментировать код первоначальной программы.)
Определение победителя: Выигрывает код, в котором исходный текст менее всего изменён по сравнению с полным первоначальным вариантом (количество добавленных символов + количество удалённых символов + количество изменённых символов), считая и строки с program и end.. Разница в больших/маленьких буквах, а также замена символа на одинаковый по начертанию (например, русское «с»/английское «c») считается за пол-символа. Если два решения имеют одинаковое количество отличий (например, ноль), выигрывает то, у которого меньше добавленного кода (в символах). Если несколько решений одинаковы и по этому критерию, выигрывает то решение, которое получит больше голосов (как обычно, «за» минус «против»).
В частности, полное совпадение кода выигрывает у неполного независимо от количества подготовительного кода.
Для того, чтобы было легче проверять ваш код, старайтесь публиковать ссылку на онлайн-компилятор с вашим кодом. Код должен компилироваться без ошибок (пусть даже с предупреждениями) и правильно работать в диапазоне входных чисел от 0 до 1000.
Продолжительность конкурса — 1 неделя.
Для исключения разночтений, при неясности в правилах пожалуйста переспрашивайте в комментариях или в чате, посвящённом code golf
Для примера, вот внеконкурсное решение на plain TeX:
ewcount\tmp
ewcount\c
\def\uncatcodeletters{\uncatcoderange{`a}{`z}\uncatcoderange{`A}{`Z}}
\def\uncatcoderange#1#2{%
\tmp=#1 \advance\tmp -1
\loop\advance\tmp 1 \expandafter\catcode
umber\tmp=11 \ifnum\tmp<#2
epeat}
\def\s{\begingroup\uncatcodeletters\shlp}
{\def~ := #1 + #2;{%
\global\expandafter\c\csname #1\endcsname
\global\expandafter\advance\expandafter\c\csname #2\endcsname
\endgroup}
\global\let\shlp=~}
\def\inp{\begingroup\uncatcodeletters\inphelper}
\def\inphelper eadln(#1);{\endlinechar=-1 \escapechar=-1
\expandafter\inphelperi\csname #1\endcsname\endgroup}
\def\inphelperi{\global
ead16 to }
\def\out{\begingroup\uncatcodeletters\outhelper}
\def\outhelper riteln(#1);{\message{\expandafter\the\csname #1\endcsname}\endgroup}
\let\DEF\def \let\END\end
\catcode`r=13 \let r=\inp \catcode`w=13 \let w=\out
\catcode`p=14 \catcode`v=14 \catcode`b=14
\let~=\catcode ~`c=13 \let c=\s ~`e=13 \DEF end.{\END}
program test;
var a, b, c: integer;
begin
readln(a);
readln(b);
c := a + b;
writeln(c);
end.
(Если кому интересно, гольфированный вариант.) Транскрипт компиляции:
~>tex golf.tex
This is TeX, Version 3.14159265 (MiKTeX 2.9 64-bit)
(golf.tex
a=5 % <-- 5 введено с консоли
b=8 % <-- 8 введено с консоли
13 )
No pages of output.
Transcript written on golf.log.
Просьба к отвечающим писать в начале решения ваш язык, количество изменённых символов и количество символов в подготовительном коде. Также просьба давать не более одного решения на ответ.
Обновление: Конкурс окончен, вот результаты.
Побеждает ответ @Mike, сумевший уложиться в 78 подготовительных символов, и не поменять ни символа в исходном коде.
Другой ответ того же автора выглядит чрезвычайно изящно (подключение паскалевского синтаксиса как внешний модуль, хей!), и почти выиграл приз зрительских симпатий, но проигрывает по количеству символов вследствие своей большей общности. Оба решения пользуются особенностью языка Perl, который в своих модулях позволяет предобработку текста на Perl самим Perl'ом. Мощный язык, мощные средства управления синтаксисом, заслуженная победа.
Второй в списке победителей — ответ @Qwertiy. Это решение продолжает идею «получить текст как строку, обработать, чтобы получился код на нужном языке, и выполнить над ней eval», с симпатичной, очень техничной и компактной реализацией (регулярки!).
Приз зрительских симпатий получает неожиданный ответ @kmv. В этом решении текст исходной программы не объявляется строкой, а «вытягивается» из кода функции! (Это, формально говоря, решение не по стандарту, но фактически в распространённых браузерах toString() работает именно так.)
Третье место получает решение @pavel с комбинацией Unix shell/C, которое обходится без eval за счёт замены строк до компиляции и использования препроцессора C. Такой подход позволяет справиться с двоеточиями, которые вызывают затруднения для препроцессора у чистых решений на C/C++.
Вообще, идея со строкой и eval оказалась наиболее популярной, её реализуют также ответы @edem Perl, построчная замена, практически интерпретация, @Red Skotina (замена строк на Питоне, построчная адаптация текста, оставаясь в рамках правил, хотя и на грани), @gil9red (то же, но более универсально, Питон), @nuts119 на C# (да, в C# можно сделать eval, вы не знали?) и @Streletz на Java (интерпретатор из сторонней библиотеки).
Тему интерпретации продолжает ещё одно решение @nuts119 на C# с использованием DataTable как arithmetic engine. Это решение, при всей его сложности, имеет дальний прицел на построение полноценного интерпретатора.
Оставшиеся решения на чистом C/C++ и Javascript/Typescript без eval вынуждены модифицировать исходный текст, хотя они смогли обойтись минимальным количеством изменений. Из этих решений наилучшие с одним удалённым символом решения @kmv (C, препроцессор, использование битовых полей) и @Qwertiy (C++, тонкости препроцессора). Интересно, что эти оба решения убирают из исходного текста соседние символы: из := убрано в первом случае двоеточие, а во втором — знак = (!).
Оставшиеся четыре решения (@Qwertiy, typescript, @pavel, C++, препроцессор (заработало больше голосов, чем победитель), снова @Qwertiy, javascript и @Grundy, C, препроцессор) меняют больше символов в исходном коде, но также интересны и стоят вашего внимания.
Большое спасибо всем, кто принимал участие в конкурсе!
Таблица лидеров: (спасибо @Grundy за адаптацию скрипта и @jfs за идею)
function getAnswers(questionId, answer_filter, page) {
return jQuery.ajax({
url: '//api.stackexchange.com/2.2/questions/' + questionId + '/answers?page=' + page + '&pagesize=100&order=desc&sort=activity&site=ru.stackoverflow&filter=' + answer_filter,
method: "get",
dataType: "jsonp",
crossDomain: true
}).then(function(data) {
if (data.has_more) {
return getAnswers(questionId, answer_filter, page + 1).then(function(d) {
return data.items.concat(d.items);
})
}
return data.items;
});
}
function getAuthorName(e) {
return e.owner.display_name
}
function process(items) {
return items.map(function(item) {
var matched = item.body.match(/\s*(.+?)\s*?,.*?(\d+),.*?(\d+)\s*?(?:[.;,(].*)?<\/h/);
if (matched) {
return {
lang: matched[1],
setup: +matched[2],
changes: +matched[3],
link: item.share_link,
author: getAuthorName(item)
};
} else {
return {
lang: "N/A",
setup: "N/A",
changes: "N/A",
link: item.share_link,
author: getAuthorName(item)
}
}
});
}
function sort(items) {
return items.sort(function(a, b) {
if (a.lang == "N/A") return 1;
if (a.changes != b.changes) return a.changes - b.changes;
return a.setup - b.setup;
})
}
function fillTemplate(sortedItems) {
$('#leadership').append(sortedItems.map(function(item, index) {
return $('').append($('| ').html(index + 1))
.append($(' | ').html(item.author))
.append($(' | ').html(item.lang))
.append($(' | ').html(item.setup))
.append($(' | ').html(item.changes))
.append($(' | ').append($('').attr('href', item.link).text('Link')));
}));
return sortedItems;
}
var QUESTION_ID = 526265,
ANSWER_FILTER = "!4*SyY(4Kifo3Mz*lT",
startPage = 1;
getAnswers(QUESTION_ID, ANSWER_FILTER, startPage)
.then(process)
.then(sort)
.then(fillTemplate);
#leadership {
border-collapse: collapse;
}
#leadership td,
#leadership th {
padding: 5px;
}
#leadership th {
text-align: center;
}
Таблица лидеров
|
Автор |
Язык |
Подготовка |
Изменено |
|
Ответ
Раз в конкурсе участвует размер, вот еще вариант (лично мне нравится меньше, потому как большая ориентировка именно на этот код): perl, подготовка 97, измененных 0 perl, подготовка 78, измененных 0 sub r{$_[0]=<>}
sub wr{print@_}
$_=q. program test;
var a, b, c: integer;
begin
readln(a);
readln(b);
c := a + b;
writeln(c);
end. ;s/\b(\w)\b/\$$1/g
;s/^\S.*|.{4}n|://gm
;eval
Тест на ideone В коде происходит:
Создаем функции r/wr аналогичные по действию паскалевским read/write ln.
$_=q. присваивает "переменной по умолчанию" весь текст до следующего знака .
Все последующие строки работают с этой "переменной по умолчанию", так как явных переменных в них не указано.
Перед переменными (однобуквенными словами) ставим знак $
Удаляем строки program, var, begin, end, части названий функций (readln=r, writeln=wr), двоеточия
Выполняем полученный текст: r($a);
r($b);
$c = $a + $b;
wr($c);
Столкнулся с тем, что требуется реализовать множественное условие, которое в других языках я бы реализовал с помощью конструкции switch-case. В Python мне приходится расписывать всё через условия if-elif-else. Это мне кажется довольно неудобным. Есть ли более удобный способ записи подобных условий? Например, у меня есть единицы измерения и в зависимости от выбранной мне нужно вернуть соответствующий множитель: def get_multiplier(unit):
if unit == 'mm':
return 10**-3
if unit == 'cm':
return 10**-2
if unit == 'dm':
return 10**-1
if unit == 'm':
return 1
if unit == 'km':
return 10**3
raise ValueError('Undefined unit: {}'.format(unit))
Ответ
Для начала, ничего особенно плохого в использовании конструкции if-elif-else нет При желании можно найти несколько альтернатив.
Использование словарей Довольно распространённый способ организации конструкции switch-case в Python — это использование словаря. Проще показать на примере: unit_to_multiplier = {
'mm': 10**-3,
'cm': 10**-2,
'dm': 10**-1,
'm': 1,
'km': 10**3
}
Для того, чтобы получить нужный множитель в этом случае требуется лишь взять значение по ключу: try:
mult = unit_to_multiplier['cm']
except KeyError as e:
# можно также присвоить значение по умолчанию вместо бросания исключения
raise ValueError('Undefined unit: {}'.format(e.args[0]))
Если вы твёрдо уверены, что значение всегда будет присутствовать в словаре, можете опустить блок try-except и быть готовым ловить исключение в другом месте. Некоторой вариацией этого подходя будет предварительная проверка значения в условии: if unit in unit_to_multiplier:
mult = unit_to_multiplier[unit]
else:
# обработка отсутствия значения в словаре
В Python принято использовать подход, звучащий примерно так: "лучше попробовать и получить ошибку, чем каждый раз спрашивать разрешение", поэтому более предпочтительный подход с использованием исключений. Если хочется использовать значение по умолчанию в случае, если ключ отсутствует, удобно использовать метод get mult = unit_to_multiplier.get('ultra-meter', 0)
Если словарь вам требуется один раз, можно объединить эти выражения в одно: unit_to_multiplier = {
'mm': 10**-3,
'cm': 10**-2,
'dm': 10**-1,
'm': 1,
'km': 10**3
}.get('km', 0)
На этом возможности этого подхода не заканчиваются. Можно использовать условные выражения в качестве ключей словаря: def get_temp_description(temp):
return {
temp < -20: 'Холодно',
-20 <= temp < 0: 'Прохладно',
0 <= temp < 15: 'Зябко',
15 <= temp < 25: 'Тепло',
25 <= temp: 'Жарко'
}[True]
Этот словарь после вычисления будет иметь два ключа True и False. Нас интересует ключ True. Будьте внимательны, что условия не перекрываются! Подобные словари могут проверять произвольное свойство, например, тип (источник примера): selector = {
type(x) == str : "it's a str",
type(x) == tuple: "it's a tuple",
type(x) == dict : "it's a dict"
}[1] # можно использовать число 1 как синоним True
При необходимости более сложных действий хранить функцию в качестве значения по каждому ключу: import operator operations = {
'+': operator.add,
'*': lambda x, y: x * y,
# ...
} def calc(operation, a, b):
return operations[operation](a, b)
Будьте внимательны при использовании словаря с функциями — убедитесь, что вы не вызываете эти функции внутри словаря, а передаёте по ключу; иначе все функции будут выполняться каждый раз при конструировании словаря.
Другие способы Приведены скорее для ознакомления, чем для реального использования.
Использование функций с шаблонными именами Создадим класс, в котором напишем несколько методов вида: def process_first(self):
... def process_second(self):
... ...
И один метод-диспетчер: def dispatch(self, value):
method_name = 'process_' + str(value)
method = getattr(self, method_name)
return method()
После этого можно использовать метод dispatch для выполнения соответствующей функции, передавая её суффикс, например x.dispatch('first')
Использование специальных классов Если есть желание использовать синтаксис switch-case в максимально похожем стиле, можно написать что-то вроде следующего кода class switch(object):
def __init__(self, value):
self.value = value # значение, которое будем искать
self.fall = False # для пустых case блоков def __iter__(self): # для использования в цикле for
""" Возвращает один раз метод match и завершается """
yield self.match
raise StopIteration def match(self, *args):
""" Указывает, нужно ли заходить в тестовый вариант """
if self.fall or not args:
# пустой список аргументов означает последний блок case
# fall означает, что ранее сработало условие и нужно заходить
# в каждый case до первого break
return True
elif self.value in args:
self.fall = True
return True
return False
Используется следующим образом: x = int(input()) for case in switch(x):
if case(1): pass
if case(2): pass
if case(3):
print('Число от 1 до 3')
break
if case(4):
print('Число 4')
if case(): # default
print('Другое число')
Использование операторов and и or Довольно небезопасный способ, см. пример: # Условная конструкция Select Case x
Case x<0 : y = -1
Case 0<=x<1 : y = 0
Case 1<=x<2 : y = 1
Case 2<=x<3 : y = 2
Case Else : y = 'n/a'
End Select # Эквивалентная реализация на Python y = (( x < 0 and 'first segment') or
(0 <= x < 1 and 'second segment') or
(1 <= x < 2 and 'third segment') or
(2 <= x < 3 and 'fourth segment') or 'other segment')
Этот способ использует короткую схему вычисления операторов and и or, т.е. то, что логические выражения вычисляются следующим образом: (t вычисляющееся как True, f вычисляется как False): f and x = f
t and x = x
f or x = x
t or x = t
Предложенный способ вычислений будет работать только в том случае, если второй аргумент оператора and всегда будет содержать True-выражение, иначе этот блок всегда будет пропускаться. Например: y = (( x < 0 and -1) or
(0 <= x < 1 and 0) or
(1 <= x < 2 and 1) or
(2 <= x < 3 and 2) or 'n/a')
Будет работать неправильно в случае 0 <= x < 1, т.к. выражение 0 <= x < 1 and 0 равно 0, и из-за этого управление перейдёт следующему аргументу or, вместо того, чтобы вернуть этот ноль в качестве результата выражения.
Использование исключений Если объявить несколько функций: import sys class case_selector(Exception):
def __init__(self, value): # один обязательный аргумент
Exception.__init__(self, value) def switch(variable):
raise case_selector(variable) def case(value):
exc_сlass, exс_obj, _ = sys.exc_info()
if exc_сlass is case_selector and exс_obj.args[0] == value:
return exс_class
return None
Здесь используется функция sys.exc_info, которая возвращает набор из информации об обрабатываемом исключении: класса, экземпляра и стека. Код с использованием этих конструкций будет выглядеть следующим образом: n = int(input())
try:
switch(n)
except ( case(1), case(2), case(3) ):
print "Число от 1 до 3"
except case(4):
print "Число 4"
except:
print "Другое число"
Рекомендуемая литература и документация по знаниям и навыкам разработки, не привязанным к конкретным языкам и платформам. Все эти книги следовало бы добавить в каждый список литературы, но мы следуем принципу DRY и сохраняем их здесь, в одном месте. Дополнения к ответу всячески приветствуются!
Данный перечень входит в поддерживаемый сообществом Сборник учебных ресурсов по программированию
Ответ
Архитектура ПО
Structure and Interpretation of Computer Programs - 2nd Edition. Harold Abelson, Gerald Jay Sussman, Julie Sussman Русский перевод: Структура и Интерпретация Компьютерных Программ. Харольд Абельсон, Джеральд Джей Сассман
Алгоритмы и структуры данных
The Algorithm Design Manual. Steven S Skiena. 2008 Русский перевод: Алгоритмы. Руководство по разработке. Стивен Скиена. 2014 г.
Introduction to Algorithms, 3rd Edition. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. 2009 Русский перевод: Алгоритмы. Построение и анализ. Томас Х. Кормен, Чарльз И. Лейзерсон, Рональд Л. Ривест, Клиффорд Штайн. 2015 г.
The Art of Computer Programming. Donald E. Knuth. 2011 Русский перевод: Искусство программирования. Дональд Э. Кнут. 2015 г.
Algorithms + Data Structures = Programs. Niklaus Wirth, 1976 (or Algorithms + Data Structures. 2004. Русский перевод: Алгоритмы + структуры данных = программы. М.: Мир, 1985, Алгоритмы и структуры данных. М.: Мир, 1989, Алгоритмы и структуры данных. Новая версия для Оберона. М.: ДМК Пресс, 2010. Старая версия книги, в отличие от новых, содержит подробно разобранный компилятор простого языка.
Проектирование и стиль кода
Design Patterns: Elements of Reusable Object-Oriented Software. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides Русский перевод: Приёмы объектно-ориентированного проектирования. Паттерны проектирования. Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидс
Patterns of Enterprise Application Architecture. Martin Fowler Русский перевод: Архитектура корпоративных программных приложений. Мартин Фаулер
Domain-Driven Design: Tackling Complexity in the Heart of Software. Eric Evans Русский перевод: Предметно-ориентированное проектирование. Структуризация сложных программных систем. Эрик Эванс
«Совершенный код» (Code Complete). Стив Макконнелл
«Рефакторинг. Улучшение существующего кода». Мартин Фаулер
«Чистый код. Создание, анализ и рефакторинг» (Clean Code: A Handbook of Agile Software Craftsmanship). Роберт Мартин
Навыки разработчика
Эндрю Хант, Дэвид Томас — «Программист-прагматик. Путь от подмастерья к мастеру»
Организация процесса разработки
Том ДеМарко — «Deadline. Роман об управлении проектами»
Роберт Мартин — «Быстрая разработка программного обеспечения»
Джоэл Спольски — «И снова о программировании».
Джо Мараско — «IT-проекты. Фронтовые очерки»
Стив Макконнелл — «Сколько стоит программный проект»
Джин Ким, Кевин Бер, Джордж Спаффорд — «Проект «Феникс». Роман о том, как DevOps меняет бизнес к лучшему».
Синтаксический разбор и компиляция
Compilers: Principles, Techniques, and Tools, Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman Русский перевод: Компиляторы: принципы, технологии и инструментарий, Альфред Ахо, Моника С. Лам, Рави Сети, Джеффри Ульман. Известна как «Книга Дракона».
Навеяно статьёй о различиях DTO, POCO и Value Object на Хабрахабре:
DTO vs POCO vs Value Object, а также
вопросом POCO vs DTO Нигде нет конкретных примеров. Приведите, пожалуйста, конкретный пример с небольшим описанием (или также примером), где и как его использовать и для чего. UPD Отличные ответы. Всем спасибо. Еще небольшой вопрос по использованию POCO. Когда и насколько рационально запихивать логику в объекты? Вот к примеру, у меня есть сервисный слой, который возвращает POCO, какие именно методы я туда могу вставить? Допустим, мне нужно валидировать Кастомера, ок, я сделал в POCO метод Validate, пока мне не нужно лезть для валидации в базу - все хорошо, но как только это понадобиться, идея уже не кажется такой хорошей. Или я не прав? Сейчас у меня приложение, где почти все действия выполняет бизнес слой, в моделях только простые методы типа GetFullName, и по сути я оперирую DTO-хами. Так вот, как уследить ту тонкую грань "что в POCO, что в сервисе" или вообще "всю логику в сервисы, оперировать DTO"?
Ответ
Представим некоторый интернет магазин. У этого магазина есть веб-интерфейс и сервер приложений, который обрабатывает логику. Некий пользователь хочет совершить какой-нибудь заказ. Для этого ему нужно выполнить ряд действий: добавить нужные товары в корзину и подтвердить заказ. Для того, чтобы это сделать, на сервере приложений может существовать класс Order public class Order
{
private ItemDiscountService _itemDiscountService;
private UserService _userService; public Order(ItemDiscountService itemDiscountService, UserService userService)
{
_itemDiscountService = itemDiscountService;
_userService = userService
} public int Id { get; set; }
public List- Items { get; set; }
public decimal Subtotal { get;set; }
public decimal Discount { get; set; }
public void AddItem(Item item)
{
Items.Add(item);
CalculateSubtotalAndDiscount();
} public void CalculateSubtotalAndDiscount()
{
decimal subtotal = 0;
decimal discount = 0;
foreach (var item in Items)
{
var currentCost = item.Cost * _itemDiscountService.GetDiscountFactor(item) * _userService.GetCurrentUserDiscountFactor();
subtotal += currentCost;
discount += item.Cost - currentCost;
} Subtotal = subtotal;
Discount = discount;
}
}
Этот класс содержит в себе данные и логику их изменения. Он не унаследован от какого-либо специфического класса из сторонней библиотеки или от какого-либо стороннего класса и является достаточно простым - Plain Old CLR/Java Object
Когда пользователь добавляет что-то в корзину, эта информация передаётся на сервер приложений, что вызывает метод AddItem в классе Order, который пересчитывает стоимость товаров и скидку, меняя тем самым состояние заказа. Нужно отобразить пользователю это изменение, и для этого нужно передать обновлённое состояние обратно на клиент. Но мы не можем просто передать экземпляр нашего Order или его копию, так как он зависит от других классов (ItemDiscountService, UserService), которые в свою очередь могут зависеть от других классов, которым может быть нужно соединение с базой данных и т. п. Конечно, их можно продублировать на клиенте, но тогда на клиенте будет доступна вся наша логика, строка подключения к БД и т. п., чего мы показывать совершенно не хотим. Поэтому, чтобы просто передать обновленное состояние, мы можем сделать для этого специальный класс: public class OrderDto
{
public int Id { get; set; }
public decimal Subtotal { get; set; }
public decimal Discount { get; set; }
public decimal Total { get; set; }
}
Мы сможем поместить в него те данные, которые хотим передать на клиент, создав тем самым Data Transfer Object. В нем могут содержаться совершенно любые нужные нам атрибуты. В том числе и те, которых нет в классе Order, например, атрибут Total
У каждого заказа есть свой идентификатор - Id, который мы используем для того, чтобы отличать один заказ от другого. В то время как в памяти сервера приложений может существовать заказ с Id=1, содержащий в себе 3 предмета, в БД может хранится такой же заказ, с тем же идентификатором, но содержащий в себе 5 предметов. Такое может возникнуть, если мы прочитали состояние заказа из БД и поменяли его в памяти, не сохранив изменения в БД. Получается, что несмотря на то, что некоторые значения у заказа в БД и заказа в памяти сервера приложений будут отличаться, это все равно будет один и тот же объект, так как их идентификаторы совпадают. В свою очередь значение стоимости - 100, номер идентификатора - 1, текущая дата, имя текущего пользователя - "Петрович" будут равны аналогичным значениям только тогда, когда эти значения будут полностью совпадать, и никак иначе. Т. е. 100 может быть равно только 100, "Петрович" может быть равен только "Петрович" и т. д. И неважно, где будут созданы эти объекты. Если их значения будут полностью совпадать - они будут равны. Такие объекты называются Value Object Помимо уже существующих Value Object типа decimal или string можно создавать и свои. В нашем примере мы могли бы создать тип OrderPrice и поместить туда поля Subtotal, Total и Discount. public struct OrderPrice
{
public decimal Subtotal;
public decimal Discount;
public decimal Total;
}
В c# есть подходящая для этого возможность создавать значимые типы которые сравниваются по значению и при присваивании целиком копируются.
UPDATE
Что касается обновленного вопроса (хоть это действительно отдельный большой вопрос, как заметил Discord): Когда мы разрабатываем приложение мы работаем с какой-либо предметной областью. Эта предметная область может быть выражена в виде некоторой модели и действий, которые меняют состояние этой модели. Все это может быть представлено в виде набора классов. Такие классы содержат в себе как данные (в виде полей классов) так и действия, которые этими данными манипулируют (в виде методов). В принципе нет никаких ограничений на размещение данных или методов по классам. Можно вообще все засунуть в один класс и это будет прекрасно работать.
Основная проблема заключается в том, что такой код будет сложнее, а значит дороже поддерживать. Так как все будет переплетено между собой - любые изменения могут привносить кучу ошибок и т.п. Поэтому, для достижения более "дешевого" кода мы начинаем его как-то структурировать, разбивать на модули и т.п. Мы можем разложить данные в одни классы, а методы в другие и это тоже будет работать и будет даже более модульно. Но все равно может нести ряд минусов. Глядя на кучу данных может быть не очевидным то, что вообще с ними может происходить или кому они могут быть нужны. Тоже самое и с кучей методов. Поэтому, чтобы было еще удобнее можно разложить данные по классам как-то сгруппировав их понятным образом. Тоже самое и с методами. Данные заказа, пользователя, товара и т.п. могут стать отдельными классами так же как и классы с соответствующими методами. Это будет еще модульнее и понятнее. Но у любого подхода есть свои плюсы и минусы. Например, в нашем интернет магазине есть различные товары, логика расчета цены которых может быть достаточно сложной.
Представим, что есть некий базовый класс Item, и множество производных классов: public class Item
{
public int Id {get;set;}
public string Name {get;set;}
public decimal BaseCost {get;set;}
public decimal Cost {get;set;}
} public class Boots : Item { ... }
public class Shirt : Item { ... }
public class Pants : Item { ... }
Так как логика у нас находится в отдельных классах, представим что есть класс ItemCostService, который умеет рассчитывать стоимость товара. Тогда,
из-за наличия большого числа различных условий он может выглядеть как-то так: public class ItemCostService
{
public decimal CalculateCost(Item item)
{
if(item is Boots)
{
item.Cost = ...
}
else if (item is Shirt)
{
item.Cost = ...
}
else if ....
}
}
И таких мест в программе, где в зависимости от конкретного типа товара должно быть различное поведение может быть много.
Конечно, это все будет работать. Но, как только у нас появляется новый тип товара, или поменяется логика обработки существующего типа товара нам
придется изменить код в большом количестве мест везде, где присутствуют такие условия. Это сложнее, чем поменять все в одном месте, дольше и чревато тем, что можно что-то забыть сделать. В данном вопросе мы говорим о языках, основной парадигмой которых является ООП. А это значит, что существует готовая инфраструктура которая поддерживает основные принципы ООП. Чтобы следовать этой парадигме и получать выгоду от готовой инфраструктуры мы можем поменять наши класс, добавив логику вычисления стоимости в них, меняя ее по необходимости в производных классах: public class Item
{
...
public virtual void CalculateCost() { ... }
} public class Boots : Item
{
public override void CalculateCost() { ... }
}
Каждый производный тип сам сможет определить логику своего поведения. Вся она будет в одном месте, рядом с данными. А какой из конкретных методов вызвать определит уже инфраструктура избавив нас от этой головной боли. В данном примере такой подход будет более удобен, т.к. у нас пропадет необходимость создавать куче if'ов по всему коду, что только упростит программу и сделает изменения более простыми. Ну и опять же - все зависит от ситуации. Серебряной пули не бывает и в различных случаях стоит использовать различные подходы, которые будут более дешевы в каждой конкретной ситуации. Еще немного про ООП и остальное можете посмотреть в моей статье тут
На данный момент изучаю шаблоны проектирования и пробую применять их на практике, но из-за небольшого опыта работы с ними и отсутствия менторства в этом деле прошу у вас помощи. Есть, например, задача - сделать стрипт для связи с людьми. Подробней:
Мы говорим что хотим связаться с пользователем определенным способом, задаем вспомогательную информацию для объекта, далее говорим отправить. В идеале вижу использование скрипта в виде с использованием фабрики и стратегии: $object = Communication::GetDriver('sms');
$object->setMsg($text);
$object->setTelephone($phone);
$object->send();
Но может потребоваться отправить совершенно другим способом, например, в соцсеть. $object = Communication::GetDriver('socialNetwork');
$object->setMsg($text);
$object->setIdUser($id);
$object->send();
Вот и вопрос, как лучше поступить? Стоит ли заморачиваться с шаблонами? Может быть, следует сделать все это отдельными классами? Это будет оправдано? При этом желательно ловить ошибки и вести лог происходящего. А если использовать шаблоны то как объединить классы? Может быть, есть методы, как сделать лучше?
Ответ
Стоит. Только нужно внести ясность. Обычно полагают, что код пишется "на один раз" или "код слишком простой". На практике часто оказывается, что "да, оно одноразовое, но надо добавить это...и это". В итоге рождается привычка сразу делать структурно - выделить классы, за которыми спрятать лапшу (точек расширения не надо, просто порядок), а в случае веба взять любой фреймворк, а не тешить себя мыслями "да тут делов то ерунда, роутинг простой, шаблоны не надо" Практически всегда есть смысл разделить на логгеры, манагеры, ридеры, чтобы оперировать более высокими уровнями абстракции и спрятать за ними детали, если вы не пишете админские скрипты или не фрилансите (написал, отдал, забыл) Главное не забывать, что шаблоны призваны решать общие проблемы общими способами. Поэтому не должно быть "ооп ради ооп" и "шаблоны ради шаблонов".
Тут многие совершают ошибку - начитаются умных книг и пытаются применить шаблон. Суть в том, что шаблоны - общие решения, которые как то названы. При решении проблем люди приходят к общим решениям, называют его и пишут в книжку. И шаблоны полезны - знание шаблонов может подсказать приемлемые варианты и мозг приучается видеть их сразу. Впрочем, даже если не знать ни одного, то на практике все равно сам их изобретаешь. Также помогают в коммуникации (назвать шаблон проще, чем пояснять, что за структура классов и как она связана).
Но решать то нужно задачи, потому на задачах и нужно акцентироваться, а не на шаблонах. Правильный путь такой: берем правило "делай все проще", добавляем в него "главная сила ооп не в 3 китах, а в гибкости объектов и возможности комбинировать их", приправляем приемами чистого кода и других парадигмами - и решаем задачу. Если видим, что подходит какой то шаблон (обычно не понять какой точно, слишком тонкая грань), то идем к нему решая задачу. Как только задача решена, то все - дальше реализовывать шаблон не нужно. Нет причины - нет кода. Неважно насколько решение получилось "классическим" - любое продолжение есть усложнение. Так что разбираться в шаблонах полезно - это как перенимать чужой опыт решения типичных проблем. И использовать полезно. Но чтобы избежать попадания в ловушку "шаблоны ради шаблонов" следует использовать прием, каким шаблон решает задачу, закодить его и остановиться.
|