Усовершенствование паттерна Flyweight в биовычислениях

    Предыстория

    Сразу извиняюсь за сложность, но сложна как сама ситуация для применения этого, так и способ решения, но получается в результате красиво и эффективно :)

    Началось с того, что описал одну проблемку о проблемах ООП. Потом случайно благодаря разговорам тут начал обдумывать паттерны проектирования. И в связи с темой «полное копирование объекта» вышел на паттерн Flyweight. Кто не знает — прошу вначале читайте о нем в Приемы объектно-ориентированного проектирования. Паттерны проектирования (Не в вики, а в оригинале).

    Основная идея там такова:

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

    Задача

    Мы рассмотрим как это улучшить на конкретном примере. О биовычислениях буду говорить очень мало — но пример будет построен на этом. Суть биовычислений попытаюсь полностью вытравить, оставив только схему.

    P.S. Если кому то интересна проблематика самих биовычислений по задаче сворачивания РНК/белков — делайте заказ напишу тогда отдельную статью.



    Итак есть объект РНК (RNA). Он является наследником более общего объекта Chain (Цепь из молекул) (например, могут быть еще ДНК, белки и т.д.). Каждое РНК состоит из множества молекул (нуклеотидов) (Molecule). В данном случае молекула может быть четырех видов (наследники Molecule) — Cytosine, Uracil, Guanine, Adenine. Каждая молекула в зависимости от типа состоит из 28-33 атомов (Atom). У каждого атома есть три расчетных угла

    public class Angles 
    {
            private float phi;
            private float theta;
            private float d;
    }
     


    Требуется на основании первичной структуры цепочки, например, aagaggucggcaccugacgu — построить трехмерную модель. Т.е. создать порядка 1000 взаимосвязанных атомов, которые в общем виде представляют собой граф. Создание этого графа занимает серьезное время.

    Дальше нужно выполнять биорасчеты. Эта цепочка принимает определенную 3D конфигурацию. Рассчитываются координаты каждого атома и его углы по отношению к другому атому. Расчеты проходят по схеме — берем последние «хорошие» состояние РНК и пробуем его «улучшить». Делаем скажем 1000 попыток повернуть одну молекулу по одному углу. Из этой 1000 выбирается только один самый лучший вариант — фиксируется (становится «хорошим» состоянием) и вычисления повторяются.

    Решение в лоб

    Собственно так оно и решалось вначале, было унаследовано из кода по проекту Rosseta@home.

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

    RNA BestRNA;
    RNA CurrentRNA;


    И нужно делать

    CurrentRNA = BestRNA.Clone();
    Calculate(CurrentRNA);
     


    Проблема именно в этом Clone(). Помним, что построить граф всех атомов очень ресурсоемкая процедура.
    А если не строить, то затрем углы всех атомов в «хорошем» состоянии.

    Классический Flyweight

    Классический Flyweight предлагает нам вытянуть свойства углы из атомов и поместить их в объекты выше. Вплоть до того, что в массив, который будет располагаться в объекте RNA. Массив этот будет как то индексироваться, чтобы по индексу можно было однозначно попасть в нужный атом.

    Тогда при клонировании нужно будет клонировать только этот массив, а граф атомов не нужно.

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

    Что делать?

    Главное вначале осознать, что мы на самом деле делаем расчеты во времени. Т.е. нам нужно сохранять траекторию атомов с течением времени. Объектная картина тогда восстанавливается.

    Дальше будет много кода с минимум комментариев, что не понятно спрашивайте.

    Итак, первое что делаем заменяем одиночные углы, на массивы времени, но с доступом по текущему времени.

    public class Angles
    {
            private float[] phi = new float[Chain.TimeHistory];
            private float[] theta = new float[Chain.TimeHistory];
            private float[] d = new float[Chain.TimeHistory];
            public float Phi
            {
                get { return phi[Chain.Time]; }
                set { phi[Chain.Time] = value; }
            }
            public float Theta
            {
                get { return theta[Chain.Time]; }
                set { theta[Chain.Time] = value; }
            }
            public float D
            {
                get { return d[Chain.Time]; }
                set { d[Chain.Time] = value; }
            }
            public void SetInitial(int argTime)
            {
                Phi = phi[argTime];
                Theta = theta[argTime];
                D = d[argTime];
            }
    }


    Далее время контролируется на уровне цепи, т.е. самого верхнего объекта. Даю кусок кода и постепенно поясняю. Вся идея будет ясна только в конце (изложение как в математике — вначале не понятно зачем нужно, и только в конце ясно зачем все это).

    public class Chain
    {
            private static Chain instance;
            public Chain(int MolCount)
            {
                instance = this;
            }
            /// Текущие время
            private int time = 0;
            public static int Time
            {
                get { return instance.time; }
                set { instance.time = value; }
            }
            public static int OldTime
            {
                get 
                {
                    int oldTime = instance.time - 1;
                    if (oldTime == -1)
                    {
                        oldTime = TimeHistory - 1;
                    }
                    return oldTime; 
                }
            }
            /// Число шагов времени, которые хранит объект
            public static int TimeHistory = 5;
     
            private static int generation;
            public static int Generation
            {
                get { return generation; }
            }
            /// Перевод времени на один шаг вперед
            protected void NextTime(int argGeneration)
            {
                Time++;
                if (Time >= TimeHistory)
                { 
                    Time = 0;
                    generation = argGeneration + 1;
                    CurrentMaxTimeID = 0;
                }
            }
     
            public void CheckTime(int argTimeID, int argGeneration)
            {
                // Не может быть поколения больше чем на 2 раза старше, и если это текущие поколение, 
                // то штамп времени не может быть больше, чем выданные штампы 
                // (такое может быть только между текущим и прошлым поколением)
                // и не может быть поколение больше текущего
                if (argGeneration < (generation - 1) || 
                   (argGeneration == generation && CurrentMaxTimeID < argTimeID) || 
                   argGeneration>generation)
                {
                    Console.WriteLine("ErrorGenerationRNA");
                    Console.ReadLine();
                }
                if (Time != argTimeID)
                {
                    Time = argTimeID;
                }
            }
     
            private static int CurrentMaxTimeID = 0;
            public static int GetNextTimeID(int argCurrentTimeID, int argGeneration)
            {
                int NextTime = -1;
     
                if (argGeneration == generation)
                {
                    NextTime = argCurrentTimeID + 1;
                }
                if (argGeneration == generation - 1)
                {
                    NextTime = 0;
                }
                if (CurrentMaxTimeID < NextTime)
                {
                    CurrentMaxTimeID = NextTime;
                }
                return NextTime;
            }
    }


    Chain выполняет паттерн Singleton, но не совсем классически. Во первых, когда создается первый объект RNA (наследник Chain), через определенный конструктор — ссылка на instance заменяется. Т.е. это не совсем единственный объект вообще, а последний созданный. И собственно, ссылка на объект не предоставляется. Получить можно только текущие время Time. Таким образом, «старые объекты» от RNA могут еще существовать, но не во времени.

    Массивы создаются на величину TimeHistory. Это относительная величина, число шагов в траектории состояний, которые важно иметь ОДНОВРЕМЕННО. В нашем пример могла быть вообще два: Best и Current. Но для оптимальности чуть увеличено до 5.

    Далее, что делается. Нужно полностью контролировать доступ к свойствам/методам RNA, чтобы заставить его работать во времени. Используем паттерн «Посредник» (но так что использующий класс не понимает это — т.е. прозрачно)

    Класс RNA переименовываем в RNARealise. А вместо реального RNA создаем «посредника»:

        public class RNA
        {
            private RNARealise body;
     
            private int TimeID = 0;
            private int GenerationID = 0;
     
            public Molecule[] Molecules
            {
                get 
                {
                    body.CheckTime(TimeID, GenerationID);
                    return body.Molecules; 
                }
            }
            public RNA(RNASeq argSeq)
            {
                body = new RNARealise(argSeq);
            }
            public RNA Clone()
            {
                body.CheckTime(TimeID, GenerationID);
                RNA NewRNA = (RNA)this.MemberwiseClone();
                NewRNA.body = NewRNA.body.Clone(this.GenerationID);
                NewRNA.TimeID = Chain.GetNextTimeID(this.TimeIDthis.GenerationID);
                NewRNA.GenerationID = Chain.Generation;
     
                return NewRNA;
            }
     
            public void Refold(int argPosition)
            {
                body.CheckTime(TimeID, GenerationID);
                body.Refold(argPosition);
            }
    }


    Что тут важно. В объект добавляется идентификатор — штамп времени TimeID и поколение объектов GenerationID. А перед каждым доступом к свойству и перед каждым вызовом времени проверяем в том ли времени находится объект body.CheckTime(TimeID, GenerationID).

    И рассмотрим как эммулируется клонирование public RNA Clone().

    в реальности клонируется только оболочка посредника, в которой присваивается новый штамп времени и поколения. Внутри будет еще перевод времени клона NextTime(argGeneration). А на уровне клонирования молекулы будет еще инициализация углов предыдущим значением

    for (int i = 1; i < FullAngles.Length; i++)
    {
        FullAngles[i].SetInitial(Chain.OldTime);
    }
     


    Что существенно меньше чем полное клонирование. А весь вызывающий код не меняется вообще — там как делалось клонирование, так и делается. Расчет имел примерно такую схему

    public void BlockFolding()
    {
        RNA saveRNA = CurrentRNA.Clone();
        locScore = NucFluctuation(saveRNA);
        // сохранение найденого лучшего состояния
        save();
    }
     
    public double NucFluctuation(RNA argRNA)
    {
        for (int j = 1; j < FragmentCount; j++)
        {
            RNA locRNA = argRNA.Clone();
            // Осуществление поворота
            Rotate.AtomAngleRotate(locRNA);
            // Расчет выгодности поворота
            RNAScore.Score(locRNA);
        }
    }
     


    Попробуете догадаться как происходит так, что углы в разное время не путаются, хотя непосредственно временем сверху никто не управляет? (вообщем оставлю это на «домашние задание» читателю, если все еще сложно понять — пойму по комментариям).
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 35

      0
      Я не нашел, а где класс Angles используется. Т.е. мне показалось, что его специально совершенствовали, но для чего. Углы принадлежат Атомам, Атомы скорее всего Молекулам… Молекулы РНК, а что такое Цепочка тогда? Можно увидеть как-то все куски кода?
        0
        Но я же пишу

        Итак есть объект РНК (RNA). Он является наследником более общего объекта Chain (Цепь из молекул) (например, могут быть еще ДНК, белки и т.д.). Каждое РНК состоит из множества молекул (нуклеотидов) (Molecule). В данном случае молекула может быть четырех видов (наследники Molecule) — Cytosine, Uracil, Guanine, Adenine. Каждая молекула в зависимости от типа состоит из 28-33 атомов (Atom). У каждого атома есть три расчетных угла


        Там примерно так

        public class RNA : Chain
        {
            public Molecule[] Molecules;
        }
        public class Molecule
        {
            public Atom[] AllAtom;
        }
        public class Atom
        {
            public Angles Angles;
        }
         


        ну и конкретные углы рассчитываются, через ряд методов — это я опускаю

        ну вы надеюсь понимаете я упрощаю.

          0
          Просто по мне код становится не читабельным и не связным, как тогда понять выйгрыш или пройгрыш в чем-то.

          Например, конструктор класса Chain содержит параметр MolCount поидее общее количество молекул, но сами молекулы только в RNA появляются если верить комментарию.

          Вы не обижайтесь плюсик я вам поставил, но помимо плюсика должна быть ещё и ясность.
            0
            Я всего лишь не хочу чтобы вы утонули в деталях. Если честно, молекулы вообще уже есть на уровне базового класса т.е. в Chain, в РНК он уже просто конкретизированы, т.е. аденины, урацилы создаются там. Дефект упрощения — уж извиняйте. Что касается MolCount, то в RNA подается первичная последовательность, а число молекул выделяется из неё и передается в Chain

            public RNARealise(RNASeq argSeq)
                        : base(argSeq.Len)
            {
                PrimaryRNASeq = argSeq;
            }
             


            Но это вроде как мелочи, не сильно относящиеся к собственно идеи. Но вы спрашиваете, я отвечу — даже если минусик поставите :)
              0
              если кодом то так

              public class Chain
              {
                  /// Молекулы в цепи
                  public Molecule[] Molecules;
              }
               
              /// Цепь РНК
              public class RNARealise : Chain
              {
                  public RNASeq PrimaryRNASeq;
               
                  public RNARealise(RNARealise argRNA)
                  {
                      PrimaryRNASeq = argRNA.PrimaryRNASeq;
                      Molecules = new Molecule[argRNA.PrimaryRNASeq.Len + 1];
                  }
              }
               
                0
                Да и наверно не обойдусь еще еще такой конструктор

                public RNARealise(RNASeq argSeq)
                           : base(argSeq.Len)
                {
                    PrimaryRNASeq = argSeq;
                    Create3D();
                }
            0
            А разве не должно быть проверки на затирание argGeneration==generation-1 && argTimeID <=CurrentMaxTimeID?
            Чем плохо наклонировать объектов, а потом написать функцию Set, которая будет копировать данные? Разве что будет использоваться больше памяти, но зато не будет проверок на границы массива при обращении.
            Кстати, что произойдет с вашим методом в многопоточной программе?
              –1
              Отвечу вначале по поводу многопоточности. Вот какая штука — программа то многопоточная, и этот код запускается в отдельном потоке. Хотел было написать, что можно запустить и второй поток с созданием новых независимых объектов. Но тут вспомнил, что появился паттерн Singleton — так сказать дефект объектной модели. Ну тогда надо поколдовать, чтобы класс Chain знал, что за поток работает — выдавал бы через Time в соответствующем потоке. Но все это лирика. Потоки не обеспечивают загрузки процессоров. Это хорошо видно у меня 2 четырехядерных процессора, и при загрузки одной программы работает только 12% процессорного времени. Можно загрузить еще одну программу — и работает тогда 24% и т.д. Поэтому если и делать параллельные вычисления, то нужно не многопоточность — а многопроцессность. Тогда одна программа выполняет главный цикл, а еще скажем 6 программ (процессов) рассчитывают асинхронно. Но им (процессам) нужно тогда постоянно воссоздавать последние «хорошие» состояние РНК (BestRNA). Т.е. 6 раз создавать граф цепи и только потом рассчитывать. (кстати, тоже самое и в потоковом представлении — только еще с невозможность эффективно распределить потоки по процессорам). Поэтому выигрыш не очень большой. Поэтому превращать это в параллельные вычисления пока не спешу… хотя когда будет отработан сам подход к биовычислениям — можно подумать о распределенном проекте. Но сейчас рано.
                +1
                Извиняюсь, написал глупость.
                0
                Что касается затирания. Если ничего не напутал. У меня есть комментарий: "(такое может быть только между текущим и прошлым поколением)". Т.е. как раз когда поколения меняются, то и происходит затирание. Т.е. если «клонирование» происходит от объекта из прошлого поколения (как правило это последние время в текущем поколении, т.е. в примере с индексом 4), то новый объект затирает данные по времени 0, но тем самым сменяется поколение.
                  0
                  Я к тому, что нужно проверить, не затерт ли объект новым поколением. Вы ведь предыдущее поколение разрешаете.
                  0
                  «Чем плохо наклонировать объектов, а потом написать функцию Set, которая будет копировать данные? Разве что будет использоваться больше памяти, но зато не будет проверок на границы массива при обращении.»

                  Если правильно понял вопрос — клонировать объекты намного дороже по времени, в этом то весь и примус.
                    0
                    Наклонировать нужно только один раз, а затем много раз вызывать Set переиспользуя объекты RNA.
                      0
                      Как это один раз. Вам нужно иметь другой объект углов, вы будите их клонировать ВСЕ ВРЕМЯ. Делаете RNA locRNA = argRNA.Clone(); и там клонируете 1000 раз объект класса Angles, а иначе вы затрете предыдущие состояние.
                        0
                        Или вы думаете создать две совершенно разных объекта RNA, в одном хранить «лучшие» значение, а во втором «расчетное», а Set вызывать копируя из «лучшего значения» в «расчетное». Так? Если нет лучше напишите псевдокодом.
                          0
                          Да, так.
                          RNA a = b.AllocateCopy();
                          a.SetFrom(b);
                          // change a
                          // calculate a
                          a.SetFrom(b);
                          // etc
                          Если схема сложнее, то можно их хранить точно так же в закольцованном массиве.
                    0
                    По поводу рассмотренного примера. Я пока бегло читал все думал: «А почему нельзя описать взаимное расположение молекул в виде динамических связей (к примеру диф. ур. типа „вход-выход“) и не запустить симуляцию дискретной модели?»
                    Описываем каждое требование по углу и взаимному расположению уравнением вида "(a0*s^2 + a1*s + a2)*Out(s) = b0*In(s)", строим дискретную модель в пространстве состояний и пускаем симуляцию. Моделируем до тех пор, когда система будет близка к равновесию…
                    Задав нормальные демпферы (коэфф. a1) можно ожидать плавный выход к равновесному состоянию => не очень большое время симуляции. Памяти будет меньше задействовано (ничего копировать не надо, не строить графы).
                    Возможно оффтоп, но что-то всплыло в голове.
                      0
                      И кстати «Приспособленец», субя по всему, хорошо впишется в данную модель. Нужно создать лишь один экземпляр динамической модели и скармливать ей при каждом вызове симуляции следующего шага вектор состояния (два числа для указанных выше уравнений), коэффициенты матричных уравнений после дискретизации и значение управления (In(s)).
                        0
                        Извините… ошибся малость. С паттерном только знакомлюсь.
                        Получается что и коэффициенты передавать не нужно будет. Просто для моделей с разными наборами коэффициентов возвращать разные экземпляры, т.е. набор коэффициентов будет ключем в реализации паттерна.
                        0
                        Боюсь, что об этом надо говорить с понимание сути биовычислений. Это если вы готовы обсуждать — мне надо написать отдельную статью. Надо?

                        А так похоже проблема в том, что уравнение вы тут никакое не напишите. Прямо не известно какие должны быть углы. Задаются определенные взаимоотношения — условия при которых образуются определенные водородные связи между молекулами. Если нужные ВСЕ водородные связи образовались — то отлично. Но для многоспиральных молекул — такого состояния я еще не разу не находил :)… алгоритм легко попадает в локальный минимум и выйти не может. Вообщем там все не просто… хотите будем обсуждать…
                          0
                          Впрочем тут моя первая статья на эту тему Целенаправленный поиск в задаче сворачивания третичной структуры РНК. Она довольно устарела, но как базис для понимания подходит. Тут зарезервировано место для второй статьи (может скоро опубликуют, тогда смогу выложить текст). Тоже пока идет подготовка к публикации, уже успела устареть.

                          Но в статье на хабре — мог бы написать так сказать более популярную статью.
                            0
                            Тема жутко интересная. Я в ней пока нуб (за исключением практики применения генетического программирования — что видимо совсем не то ), но очень хочется «просвелеть». Так что люто заплюсую за «по-простому о сложном».
                            Насчет уравнений… Если условия нелинейны, типа «if (x<0) then f1(x) else f2(x)» и т.п., то наверное действительно трудно будет применить то, что я подразумевал. Хотя есть смежные реализации динамических систем с нелинейностью. Вплоть до рекуррентных нейросетей. Просто я люблю самоорганизацию, и если корректно описать, то, возможно, удастся запустить симуляцию в поисках равновесного состояния.
                            Если найду время освоить суть задачи, возможно удастся изложить свой вариант решения (т.к. я понимаю, предпочитаю и умею). Даже если вариант будет нежизнеспособен, возможно это наведет кого-нибудь на еще одно решение.
                              0
                              ГА и рекуррентные нейросети отдыхают… проверено. Я пришел к выводу, что ИИ-методы тут малопригодны, хотя сам по специализации как раз ИИ-шник. Наиболее перспективно тут — теория игр, эта задача по сворачиванию очень похожа на игры в шахматы. Напишу скоро поясняющую статью.
                      +1
                      Мне очень не нравится получившаяся архитектура — вы много говорите о чистоте ООП и самым грубом образом нарушаете инкапсуляцию статическими полями. Класс Chain вообще солянка из приватных полей объекта и статических оберток, честно не понял какой в этом смысл. Такой код очень сложно отлаживать и поддерживать.

                      Поведение объектов не очевидно и зависит банально от создания ещё одного инстанса класса Chain. Кстати, самый простой тест на хорошую архитектуру — перенесите один любой класс в другой проект, как есть. Если программа не собирается — значит явно есть косяк )

                      По-моему, у вас тут горе от ума — суть ООП не в том, чтобы смоделировать объекты реального мира один в один как есть. Нужно создать гибкую, ясную и производительную программную модель.

                      Ну и, однозначно, нужно избавляться от статических полей, с ними это пример как не надо делать.
                        0
                        Ваше негодование по статическим полям я понимаю, это нарушение ООП согласен. Но в имеющихся условиях это тем не менее меньшие зло.

                        Вот как бы вы это исправили? Как какому то объекту знать какое сейчас текущие время? Предложите.

                        А с этим: "суть ООП не в том, чтобы смоделировать объекты реального мира один в один как есть" не соглашусь. Не возможно создать гибкую, ясную и производительную программную модель, если абстракция реального мира не соответствует вашей модели. Еще смотрите DDD.

                        Ну, и затем примененный усовершенствованный паттерн легко превратить в библиотеку, которой передается класс, указываются «свойства во времени», и объект начинает жить во времени. Наружу видно только быстрое клонирование в указанных случаях. Внутри библиотек делается создается динамический код, через возможности отображения в .Net — и все готово.

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

                          Наоборот, это то, что полностью ломает всю концепцию ООП. Решается это очень просто, например, класс Angles возвращает массивы, вместо одного значения, а уже вызывающий код сам выбирает какое из значений ему нужно. Вариантов много, но так как вы сделали лучше не делать… особенно если хотите это оформлять в виде библиотеки.
                      0
                      И ради чего вы предлагаете возвращать не нужные массивы вызывающему коду? Да еще, чтобы он не знамо как выбирал то, что ему нужно — он тоже не знает какое время. Вы и его будите учить тому что не надо — передавая ему текущие время?

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

                      Если сделать то как вы хотите — оформить в виде библиотеки уже нельзя — пользовательские классы начинают знать о времени, «выбирать». Это их перегружает, и нельзя отделить собственно получившийся паттерн от предметного кода.
                        –1
                        Если хотите оставить текущий API, можно пробрасывать Chain через конструктор Angles, и уже на его Time завязываться.
                          0
                          Не серьезно. Какие — то углы начинают агрегировать ВСЮ цепь. Ради чего? Вы предлагает создать двухсторонние ссылки — это еще БОЛЬШАЯ цена.

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

                          А вообще какие у вас претензии к статическим членам, что тут не соответствует ООП?
                            0
                            Нарушение инкапсуляции, разве не очевидно? Ваше глобальное время получается слишком глобальным вот в чем проблема. В один момент времени можно работать только с одним экземпляром класса, все это чревато трудно-уловимыми багами в дальнейшем. Особенно если вы планируете выложить это в общий доступ (за что, кстати, респект).

                            Это ваше дело как строить свою программу, но выкладывать такое в хаб «Совершенный код» это уже слишком. Прошу прощения за резкость, но я много времени потратил на ковыряние в унаследованном говно-коде и такие «проблемки» сразу бьют по глазам.
                              –2
                              Ваша резкость была бы уместна, если бы предложили лучший выход. А так пока увы — мимо.
                                –2
                                И в «Совершенным коде» это потому, что это лучшие решение из возможных. Можете не соглашаться, но вы не берете во внимание несколько аспектов. А статические члены — это меньшие зло.

                                Давайте подумаем спокойно. Нарушение инкапсуляции кого? Чем это проблема?
                                  0
                                  Ну вот посмотрите сами: вы предлагаете иметь 1000 ссылок от атома к цепи, и/или 1000 раз передавать атомам локальное время в качестве параметров. И это чем то лучше, чем использовать глобальную переменную уровня цепи (chain)?

                        Only users with full accounts can post comments. Log in, please.