Pull to refresh

Comments 54

Emit Mapper не смотрели? Местами упрощает жизнь
Вы об этом?
Нет, раньше не натыкался. Как у него с производительностью?

Я так подозреваю что на небольших объектах не стоит тратить время на emit.
промахнулся, коммент ниже
Авторы говорят что быстрый (сам не тестировал):
Handwritten Mapper: 475 milliseconds
Emit Mapper: 469 milliseconds
Auto Mapper: 205256 milliseconds

В сети много сравнений производительности.
На деле, скорее всего, чуть медленнее, чем руками. Но, явно, удобнее.
Кстати, ради эффективности полного копирования, иногда приходится отказывать от принципов ООП.

См. Границы применения ООП
Объяснение постом ниже, почему минусуем?
Здесь я на одном конкретном примере покажу границы применения объектно-ориентированного программирования. Данную статью, я пишу от первого лица, так как это частный опыт конкретного человека. При этом вам будет важно знать, что я строгий стороник ООП. Все что я программирую, я программирую строго в этой концепции и не приемлю никакие другие, и любое совмещение. До некоторого времени, меня это совершенно не подводило, и по роду своей деятельности, я мог создать любую объектную декомпозицию, и чем более я следовал объектным принципам (а их далеко не все преподают, пишут в книгах, и тем более редко применяют) — тем более строгая, ясная и действенная архитектура получалась. Поэтому с одной стороны, думаю читатель понимает, что критиковать ООП я хотел бы меньше всего, как это модно в определенной среде делать (типа «да, лишь один из подходов, но …»). Поэтому написанное не нужно понимать как критику, вместо этого, чтобы хорошо владеть инструментом, идеологией, всегда полезно знать границы применимости. Это позволяет еще более эффективно воспользоваться ООП, зная когда ты приближаешься к её границам.

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

Итак имеем:
Число объектов порядка ≈1000
Каждый из объектов содержит несколько свойств
Объекты образуют дерево. При этом каждый объект имеет ссылки на последующие узлы дерева и обратную ссылку на своего родителя
Существует массив, в котором линейно находятся все узлы дерева
Существуют ряд других массивов, которые по определенной логике содержат определенные узлы этого дерева
Таким образом существует граф с сильно связанными между собой узлами

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

Теперь специальные условия:
Такой граф образуется в программе по определенным предметным правилам, при этом для его построения нужно провести существенные вычисления
Существует управляющий класс, который по определенной логике рассчитывает значения свойств каждого из объекта
Расчет зависит от связей с другим объектами
Важно, что основной цикл динамики программы заключается в нахождении такого набора свойств, который отвечает определенным условиям
Поэтому возникает необходимость в создании копий (клонировании) всей структуры из ≈1000 объектов, произведении вычислений над этой копией, и если она не удовлетворяет условиям, то удаляется и возвращаемся к начальной структуре. И далее по циклу

Проблема возникает в таком малозаметном месте как создание копии. Тут нужно понимать, что в C# по умолчанию клонируются только свойства объекта, а связи с остальными объектами нет. Точнее копируются ссылки на связанные объекты. Так как нам нужно провести полное клонирование, то скопированные ссылки будут указывать на старые объекты, которые еще не проклонированы. А так как у нас граф, то нельзя постепенно шаг за шагом провести клонирование, так как мы попадем в петлю. Если делать произвольные разрывы, то мы не проконтролируем как в каких объектах обновились связи с другими, так как еще не будет проклонированы все объекты, с которым связан данный.

Логика же организации этого графа достаточно сложна и уже выполнялась. Но у нас нет ни какого другого варианта, как снова её повторить, так как в принципе эта логика и указывает какой объект с каким связывать.

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

Таким образом, чтобы начать расчеты данных, мы вынужденны проводить клонирование структуры, которая в принципе не меняется.

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

Таким образом, выигрыш по времени составляет порядка ≈2-3 раз. А значит чистота соблюдения объектных принципов стоит замедления программы как минимум вдвое.
«При этом вам будет важно знать, что я строгий стороник ООП. Все что я программирую, я программирую строго в этой концепции и не приемлю никакие другие, и любое совмещение. До некоторого времени, меня это совершенно не подводило, и по роду своей деятельности, я мог создать любую объектную декомпозицию, и чем более я следовал объектным принципам (а их далеко не все преподают, пишут в книгах, и тем более редко применяют) — тем более строгая, ясная и действенная архитектура получалась. „
“Узкий специалист подобен флюсу — полнота его одностороння».

Судя по всему, ваша задача прекрасно бы решалась в терминах функционального программирования (как и многие другие вычислительные задачи).
Расскажите, как же она могла бы решиться в терминах функционального программирования? (очень сомневаюсь)
А очень просто. В функциональном программировании (обычно) используются immutable структуры данных, поэтому при каком-то расчете по графу никому не придет в голову менять свойства в этом графе, а будет порожден новый. Как следствие, проблемы клонирования нет как таковой, ей негде взяться.

PS Вообще же, клонирование графа произвольной сложности решается паттерном visitor с отслеживанием посещенных объектов. Это если в объектной парадигме.
«никому не придет в голову менять свойства в этом графе, а будет порожден новый»

т.е. будет выполнено полное копирование — и даже той части, которая постоянна. Замечательно, и плакало ускорение в 2-3 раза. Т.к. чтобы породить овый граф уходит УЙМА времени.

По мне, так лучше на ассемблере кодировать, и то будет понятнее, чем в терминах функционального программирования. И спрашивается ради чего?
Нет, обычно новая копия содержит ссылку на измененную часть и постоянную часть исходной структуры. Никакого лишнего копирования не происходит при этом.
«т.е. будет выполнено полное копирование — и даже той части, которая постоянна.»
Нет. (x, y) => new {x, y+2) «копирует» x только тогда, когда это value type (а копирование value type сравнительно дешево). А если это сложная сущность, то копируется ссылка (учитывая неизменность сущности это как раз не имеет побочных эффектов, чтобы избежать которых в вашей схеме вы делаете клон).

«Т.к. чтобы породить овый граф уходит УЙМА времени.»
Ненене, не надо путать. Вы писали, что построение графа дорого потому, что для этого делается расчет. Но вы этот расчет уже сделали (когда строили граф). А теперь вам надо делать новый расчет (по вашим же словам). Соответственно, вы тратите время на обход существующего графа (один раз), расчет (новый) и присвоения. Самая тяжелая часть — расчет, которого вам все равно избежать не удастся.

«По мне, так лучше на ассемблере кодировать, и то будет понятнее, чем в терминах функционального программирования»
Скажите — так, ради интереса, — а вы ни LINQ (в .net), ни SQL (в БД) не используете?
Первый раз — действительно строиться граф (делается обход один раз). Далее оказалось, что глубокое копирование из-за запутанности графа или невозможно из-за циклов, или если искусственно рвать, то занимает еще больше времени, чем расчет по новой.

Поэтому затем и приводится другое решение (названное вынужденным нарушением ООП), граф не меняется, не строиться по новой, но клонируется класс выше Молекула, но она ссылается на те же Атомы. Атомы имеют свойство Угол — это забивается, но при откате назад в Молекуле есть массив, который позволяет восстановить этот угол. Но тут ниже я описывают только что придуманное третье решение.

Поэтому та часть расчета по формированию графа как раз не делается больше одного раза. А вот углы да — рассчитываются многочисленно…

LINQ — не использую принципиально. В хранимых процедурах естественно использую T-SQL например.
«Далее оказалось, что глубокое копирование из-за запутанности графа или невозможно из-за циклов, или если искусственно рвать, то занимает еще больше времени, чем расчет по новой. „
… говорят же вам, использовали бы функциональное решение — не было бы проблемы. Просто “углы» были бы расчетными данными, а все остальное — неизменными.

«В хранимых процедурах естественно использую T-SQL например. „
А вас не смущает тот факт, что вся его часть, связанная с выборками — это функциональный язык?
Нет, не смущает. Кроме того, вложенных select`ов я пытаюсь избегать, а в операторе

select * from MyTable — поясните где тут функциональный язык?

Использование в одном, хорошо понятном месте, не означает, что это нужно использовать там, где не надо.
«select * from MyTable — поясните где тут функциональный язык?»
Везде. Все это выражение — это функция, имеющая аргументы (0) и возвращаемый результат (рекордсет). И, что характерно, immutable.
А мне почему-то кажется, что второй вариант — и есть правильная декомпозиция предметной области. Потому как такая совокупность свойств связанных объектов — это характеристика связующего их графа, но не их самих.

На примере. Есть несколько лампочек и несколько батареек. Свойства лампочек — номинальное напряжение и номинальная мощность. Свойства батареек — внутреннее сопротивление и ЭДС. Реальная же мощность каждой такой лампочки зависит от схемы включения всего этого добра, и характеризует не столько лампочку, сколько схему.
Немного о предметной области. Класс РНК, состоит из нуклеотидов, каждый нуклеотид из атомов. Атомы связаны между собой ковалентными связями. Также они описываются определенными углами.

И вот как вы предлагаете выделить от сюда схему?

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

Совершенно не утверждаю, что подобная декомпозиция удобнее в обработке, выигрышнее по памяти, производительности или что-то в таком духе. Просто мне кажется, что именно что-то такое вы имели ввиду во втором своём варианте. И, как по мне, это самое что ни на есть ООП.
Примерно так я и имел в веду. Но это нарушение ООП. Углы — это свойство атома — то как он расположен в пространстве по отношению к другим атомам. А мы отрываем эти свойства из объекта. И проблема была именно при полном клонировании РНК. В памяти развертывался только один граф связи атомов (а как их связать между собой целая наука). РНК, конечно имело массив для быстрого доступа ко все углам и координатам атома. Но клонировались только свойства атомов, т.е. эти массивы, и верхний уровень иерархии РНК-Молекулы, но подложка связей атомов оставалась одна и та же. Т.е. получалось что разные объекты Молекулы, ссылались на одни и те же атомы. Т.е. пришлось пойти на нарушение ООП — хотя у разных молекул, конечно, разные атомы.
Впрочем, похоже речь о паттерне проектирования «Приспособленец» (Flyweight) — все же это вынужденная мера.
Но нарушение ООП в этом паттерне прямое — локальные свойства «атомов» выносятся на уровень глобальных данных «молекул». Ведь дико даже звучит — координаты атома не являются свойством атома.
У меня немного другие соображения. Я убеждён, что определение корректности состояния объекта должно лежать на нём самом. И интерфейс не должен позволять привести объект в некорректное состояние. И, в том числе и это, — именно то, зачем нужна инкапсуляция. Если же и связи, и углы являются свойствами атома и выставляются извне, то получить такое состояние запросто.

Если же взаимосвязь атомов — это свойство молекулы, её можно спроектировать так, что её состояние будет всегда корректно.
Знаете я кажется придумал, как и птичку съесть и рыбкой закусить.

Свойства, определяющие состояние объекта, как не крути должны быть у объекта. Но есть ряд свойств, которые не меняются со временем, а есть те которые меняются. Так вот для таких свойств как координаты и углы, нужно в объекте хранить массив в зависимости от времени. А также ввести у объекта свойство текущие время CurrTime. Массив coord[t] будет private, а public свойство Coord будет предоставлять get {return coord[CurrTime];} и set {coord[CurrTime]=value;}. Т.е. свойство будет по прежнему инкапсулировано в том же объекте, но в зависимости от времени может менять значения, причем они не затеряются на столько шагов времени сколько нужно. Остается только обновлять время, но тут нужно реализовать паттерн Наблюдатель.

Вышестоящие классы (в примере выше РНК) при расчетах, вместо того, чтобы делать глубокое клонирование, просто сменяют время системы (вперед при следующей попытки рассчитать или назад при неудачной попытке). А все «атомы» ловят изменение времени и уже возвращают другое состояние.
Проблема глубже: далеко не каждый алгоритм поддается объектной декомпозиции, и, как следствие, так называемая «чистота соблюдения объектных принципов» зачастую попросту невозможна.

Приведу более простой пример, чем расчет РНК: решение СЛАУ. Точнее, задачи, сводящиеся к решению СЛАУ. Например, расчет электрической цепи в установившемся состоянии.

Да, можно (иногда даже нужно) каждый элемент цепи представить в виде объекта. Более того, тут даже есть где развернуться паттерну Composite — так, РИН (реальный источник напряжения) «состоит» из ИИН и внутреннего сопротивления, включенных последовательно. Но проблемы начинаются уже на этапе определения свойств, которые стоило бы вынести в базовый класс элемента цепи. Так, у всех однородных участков цепи основным свойством является сопротивление. В то же время, у ИИТ сопротивление бесконечно велико, а определяющим параметров является сила тока…

Видел я программу, которая рассчитывает электрическую цепь методами ООП: цепь делится на участки, которые могут быть включены параллельно либо последовательно… Да вот только мостовую схему в эту программулину просто не удалось ввести.

В общем, как ни крутись, а от решения СЛАУ методом Гаусса или итераций ООП никак не спасет. В определенный задачах, в программе в любом случае будет оставаться некоторый кусок структурного кода, пусть и с объектно-ориентированным интерфейсом.

Кстати, примером подобных кусков кода являются структуры данных стандартной библиотеки. Приглядитесь к реализации красно-черных деревьев — где там ООП-то?
Главное — не впадать в крайности
Общий вывод из нашей поддискуссии: Если вам понадобилось глубокое клонирование — подумайте еще раз, может можно так спроектировать классы, чтобы это было просто не нужно.
Конечно можно подумать и перекроить. Только статья не о том нужно это или нет, а про возникающие проблемы при клонировании.
«или примите соглашение при котором обращение ко внутренним полям происходит только через this»

не делайте так никогда, объект должен легко обращаться к своим свойствами. Вместо этого примите соглашение, что параметры всех методов имеют префикс «arg», тогда вы никогда не спутаете свойства с параметрами, это же вас предостережет от присваивания в параметры (что делать также не рекомендуется).
Придерживаюсь такого же принципа. Только правило именования параметров другое.
«Вместо этого примите соглашение, что параметры всех методов имеют префикс «arg»»
Эмм. А чистота интерфейса уже никого не волнует?

Вообще-то, давно придуманы стандарты именования, чего велосипед-то изобретать?

«это же вас предостережет от присваивания в параметры (что делать также не рекомендуется).»
Это еще почему? Во всех приличных языках параметр является локальным по отношению к методу и присвоение ему чего бы то ни было является внутренним делом метода и никого не волнует. А если кто-то говорит о присвоении свойствам параметра (или вызове методов параметра, или любых других операциях, изменяющих состояние параметра) — так это (а) нормальная операция и (б) никто от этого не застрахован, какое бы именование не придумывалось.
Угадайте что выведен на экран следующий код на C#

public class Test1
{
public Test2 T2 = new Test2();
}

public class Test2
{
public string A="";
}

public class Test3
{
public void Run(Test1 argTest)
{
argTest.T2.A = «Run»;
}
}

Test1 T1 = new Test1();
Test3 T3 = new Test3();
T3.Run(T1);

Console.WriteLine(T1.T2.A);
А зачем угадывать, если и так понятно, что вы меняете состояние объекта? Я же специально написал: «если кто-то говорит о присвоении свойствам параметра (или вызове методов параметра, или любых других операциях, изменяющих состояние параметра) — так это (а) нормальная операция и (б) никто от этого не застрахован, какое бы именование не придумывалось.»

Просто подумайте, что в любом методе разумной длины никто не мешает сначала сказать var t = argT; а спустя 10-20 строк t.A = «smth».

От этого спасает только работа с неизменными данными. Или статический анализ кода. Или применение мозга.
Вот чисто интересно, зачем делать var t = argT;? Если такое происходит — это уже звоночек
Такое иногда делают в качестве поясняющей переменной. В книге Рефакторинг, Мартина Фаулера этот прием хорошо описан. См: (Introduce Explaining Variable)
Это некорректный пример. Introduce Explaining Variable делают для упрощения выражения, тут же совсем другое — присваивание параметрам с одной стороны, т.е надо сделать рефакторинг Remove Assignment to Parameters, а с другой стороны тут введение избыточной локальной переменной (в отличии от Introduce Explaining Variable без изменения семантики), т.е. от неё надо просто избавится.
«Вот чисто интересно, зачем делать var t = argT;?»
Чтобы проводить с ним операции. Code Complete, 7.5. How To Use Routine Parameters.

«Данные изменять нужно, поэтому невозможно работать с неизменяемыми данными.»
Невозможно? System.String в .net смеется над вами.

«Просто изменять их нужно в нужном месте, а не в результате непреднамеренной ошибки.»
Вот это «нужное место» — внутри метода.

«Наличие префикса показывает, что это аргумент и с ним надо работать только на чтение. „
… а если мне надо его изменить, и это — нужное место? Code Complete, ibid — можно маркировать параметры по типам (i_, m_, o_), но это (а) не обязательно и (б) дважды необязательно в языках, где это маркируется на уровне сигнатуры метода (ref и out в c#).

А использование arg* банально избыточно — вносит никому не нужный шум как внутри метода, так и в его интерфейсе, — и при этом все равно не выполняет нужной задачи, т.е. не отделяет места, где операции выполнять можно, от мест, где операции выполнять нельзя.
я вам выше показал, где изменяется свойство, но при этом ref и out не используется.

Если вам нужно его изменить — это будет свойство класса, а не аргументом метода.
«Если вам нужно его изменить — это будет свойство класса, а не аргументом метода. „
Вот именно поэтому ничего страшного в test.T2.A = “blahblahblah» нет, и не важно, мне test пришел как аргумент метода, или любым другим способом.

Вообще, я отдельно замечу, что очень опасно путать _изменение аргумента_ (то, что в c# происходит только с ref и out), и _операцию с аргументом_ (в частности, test.Calc()). Если в отношении первого есть ряд, так скажем, вопросов и заморочек, то со вторым все понятно — это делается на регулярной основе.

(Кстати, вот опять-таки: вы намеренно игнорируете функциональный подход, а вот в нем-то этой проблемы нет изначально.)
Вам, кажется, что ничего страшного. Я же считаю такое изменение параметров — плохим стилем программирования. Конечно, я не сноб, и частенько это делаю в мелких программах, но не тогда когда кодирую серьезные программы.

Изменять так свойства объектов — это плохой тон. Т.к. ее работа не соответствует тому, что она возвращает. И работает не над своим объектом. Т.е. уже по сигнатуре нельзя сказать, что сделает этот метод.
«Изменять так свойства объектов — это плохой тон. Т.к. ее работа не соответствует тому, что она возвращает. И работает не над своим объектом. Т.е. уже по сигнатуре нельзя сказать, что сделает этот метод. „
Вообще-то, то, что вы сейчас описали — это функциональная парадигма (в данном контексте — stateless). А для ООП, как в первую очередь stateful парадигмы, как раз и характерно то, что конкретный метод может изменить некое состояние, и это изменение не будет отражено в его интерфейсе.

Собственно, до тех пор, пока мы работаем с изменяемыми объектами, у вас просто нет иного выхода — вам все равно _надо_ изменять объекты, которые изначально вам не принадлежали (откуда-то пришли), поэтому вопрос, _где_ вы это делаете — вторичен.

В частности, ридер, построенный поверх потока, может этот поток закрывать при своем закрытии. Вас это удивляет? А это нормально.

Вообще же, не пытайтесь быть святее МакКоннела.
«Вообще-то, то, что вы сейчас описали — это функциональная парадигма (в данном контексте — stateless). А для ООП, как в первую очередь stateful парадигмы, как раз и характерно то, что конкретный метод может изменить некое состояние, и это изменение не будет отражено в его интерфейсе.»

Ну, что Вы — это все имеет куда более элементарные корни — это признак хорошо структурированной программы из парадигмы структурного/процедурного программирования. ООП просто вбирает этот принцип на уровне стиля программирования, аналогично правилу — одна точка входа, одна точка выхода из метода.

Поэтому конечно, хорошие новое (т.е. функциональная парадигма), хорошо забытое старое.
«ООП просто вбирает этот принцип на уровне стиля программирования, аналогично правилу — одна точка входа, одна точка выхода из метода. „
А вы его правда придерживаетесь? Исключений не бросаете, ранние возвраты не используете?

“Поэтому конечно, хорошие новое (т.е. функциональная парадигма), хорошо забытое старое. „
Функциональная парадигма малость постарше ООП (где-то так 30-ые годы XX, если я не ошибаюсь).
МакКоннела — это тот который книгу «Совершенный код» написал? О, я тут почитал выборочно — там такое варварство про ООП написано… но это отдельная тема, пока нету кармы :)

Поэтому я не пытаюсь — у меня свой котелок :)
Ну как бы ему доверия немножко побольше, чем вам. Хотя бы потому, что он опирается на hard data.
Данные изменять нужно, поэтому невозможно работать с неизменяемыми данными. Просто изменять их нужно в нужном месте, а не в результате непреднамеренной ошибки. Наличие префикса показывает, что это аргумент и с ним надо работать только на чтение.
«придуманы стандарты именования» — кем и какие? чем они лучше?
Например, Microsoft (если мы о .net). МакКонел тоже, в общем, об этом не молчал.

А лучше они тем, что не засоряют внешний интерфейс объекта.
Ну, есть такая книжка Современная практика программирования можно сказать от Microsoft, Франческо Балена и Джузеппе Димауро

Там несколько упрощенно, но даже там рекомендуют именовать параметры с маленькой буквы (чтобы не путать с публик свойствами, который именуются с заглавной). Мы лишь уточнили, что это более точно значит.
«Там несколько упрощенно, но даже там рекомендуют именовать параметры с маленькой буквы (чтобы не путать с публик свойствами, который именуются с заглавной). Мы лишь уточнили, что это более точно значит. „
Я что-то никак не вижу, чтобы это как-то означало “начинайте параметры с arg». Более того, если посмотреть на интерфейсы в .net, то явно видно, что параметры методов с arg не начинаются.
Не главное, какой префикс, главное чтобы можно было отличить аргумент метода по его названию от других. А они это делают — только очень бегло и путано. Мы делаем лучше, а не ориентируемся только на чужие стили программирования. И для этого как я показал выше есть аргументы.
«Мы делаем лучше, а не ориентируемся только на чужие стили программирования.»
Интересно, кто это «мы», и почему вы так уверены, что сделаете лучше, чем весьма большое сообщество разработчиков .net?
Статический метод. Подходит для копирования любых объектов
        public static object CloneObject(object obj)
        {
            if (obj == null) return null;

            Type t1 = obj.GetType();
            object ret  = Activator.CreateInstance(t1);

            var properties = t1.GetProperties().ToArray();
            for (int i = 0; i < properties.Length; i++)
            {
                properties[i].SetValue(
                        ret,
                        properties[i].GetValue(obj)
                    );
            }
            return ret;
        }
Sign up to leave a comment.

Articles