Caché + Java + Flex. Особенности интеграции технологий. Часть 3

    Как и было обещано во второй части эта статья посвящена исключительно практическим примерам, демонстрирующим особенности взаимодействия Caché + Java + Flex. Начнем с преобразования типов данных на протяжении всей связки. Мы не стали заострять внимание на преобразованиях таких типов данных, как Integer, Float, String, Boolean, поскольку данные типы идентичны в Caché и Java (а в ActionScript все числовые типы переходят в Number). Другое дело коллекции объектов. Их преобразование проходит достаточно нетривиально, поэтому остановимся на них подробнее.


    Преобразование коллекций


    "
    Рассмотрим преобразования коллекций на протяжении всей связки Caché + Java + Flex на примере списка дисциплин в определенном цикле. В качестве примера коллекции рассмотрим отношения — вид связей между классами Caché, реализованные как свойства типа Relationship, с определенным видом поддержки целостности. Подробнее об отношениях в Caché.
    Class IKIT.cCicl Extends %Persistent {
    …
      Relationship ListOfDisc As IKIT.cDiscipline [Cardinality = many, Inverse = Cicl];
    …
    }
    

    В сгенерированной java-проекции (пакет «Java c-classes») данное свойство представлено объектом класса com.intersys.classes.RelationshipObject из библиотеки cachedb.jar.
    public com.intersys.classes.RelationshipObject getListOfDisc()  throws com.intersys.objects.CacheException {
      com.intersys.cache.Dataholder dh = mInternal.getProperty("ListOfDisc", true);
      com.intersys.cache.CacheObject cobj = dh.getCacheObject();
      if (cobj == null) return null;
      return (com.intersys.classes.RelationshipObject)(cobj.newJavaInstance());
    }
    

    Данный класс имеет стандартный метод asList, который принимает RelationshipObject и возвращает List, что значительно облегчает работу со списком в дальнейшем.
    В m-classes используются листы (List listOfDisc), поэтому необходимо преобразовать каждый объект c-classes из RelationshipObject в соответствующий объект m-classes. Данное действие происходит в классе CacheTransform.
    for (int i=0;i<cur.getListOfDisc().size();i++) {
      res.getListOfDisc().
      add(TranformDi((cDiscipline)cur.getListOfDisc().asList().get(i)));
    }
    // cur – объект класса cCicl, res – объект mCicl
    

    Далее выполняется проецирование m-классов во flex модуль по заданным в POM-файле для GraniteDS параметрам. GraniteDS по умолчанию использует для проекции типа List (в Java) соответствующий тип ListCollectionView (в ActionScript). Например, так будет выглядеть проекция списка в сгенерированном AS-классе mCiclBase.
    [Bindable]
    public class mCiclBase implements IExternalizable {
    …
    protected var _listOfDisc:ListCollectionView;
    …
            public function set listOfDisc (value: ListCollectionView):void {
                _listOfDisc = value;
            }
            public function get listOfDisc():ListCollectionView {
                return _listOfDisc;
            }
    …
    }
    

    Для дальнейшего упрощения работы непосредственно в ActionScript использовался класс ArrayCollection, который является классом наследником ListCollectionView и реализует стандартный flex интерфейс Ilist (cur.listOfDisc = new ArrayCollection();), который в случае необходимости можно легко преобразовать as ListCollectionView.
    Обратное преобразование проходит по той же схеме. Изменения, происходящие в AS-проекции через GraniteDS, также выполняются в соответствующих классах пакета «Java m-classes». Преобразование же объектов коллекции из java m- в java c- классы происходит в классе CacheTransform в функциях с префиксом Rev. В нашем случае – в функции RevTranformCi(mCicl cur,Integer act), которая принимает объект m-класса и действие (перезапись или создание), записывает объект в БД и возвращает объект c-класса. Преобразование листов происходит в данной функции следующим образом.
    for (int i=0;i<cur.getListOfDisc().size();i++) {
      if(cur.getListOfDisc().get(i).getId()!=null) {
        res.getListOfDisc().add(RevTranformDi(cur.getListOfDisc().get(i),1));
      }
      if(cur.getListOfDisc().get(i).getId()==null) {
        res.getListOfDisc().add(RevTranformDi(cur.getListOfDisc().get(i),2));
      }
    }
    

    После этого вызывается стандартный метод _save у объекта c-classes, который сохраняет данные в базу Caché, при этом приводит тип List в соответствующий тип в Caché com.intersys.classes.RelationshipObject.
    Теперь, когда с коллекциями все понятно, перейдём к примеру, который демонстрирует добавление и удаление объектов.

    Пример добавления и удаления объекта


    Рассмотрим работу с интерфейсом сервисов на примере добавления и удаления учебного плана. Первым создаётся объект «учебный план» в AS-проекции класса mCurriculum. Создание этого объекта выполняется на стороне клиента, затем он передаётся на сервер и сохраняется в БД. Для этого вызываем через объект реализующий интерфейс сервисов сервера метод addOneCurr.
    userService.addOneCurr(cur, k);
    

    где сur – объект класса-проекции mCurriculum, созданный во flex, а k – переменная отвечающая за выбор действия (добавление или редактирование).

    Здесь необходимо помнить, что GraniteDS посылает запросы асинхронно и, если необходимо выполнить какое-либо действие строго после завершения функции, его нужно помещать непосредственно в функцию-обработчик результата. Например:
    userService.addOneCurr
       (
         cur,
         k,
         function(e:TideResultEvent):void
               {
                    Alert.show("Добавление завершено");
                    updateOneCur(curCurriculum.id);
               },
         function (e:TideFaultEvent):void 
              {
                    Alert.show(e.fault.faultDetail);
              }
        );
    

    Данная функция выведет сообщение «Добавление завершено» строго после удачного завершения функции addOneCurr.
    Ниже приведён код функции addOneCurr в классе-проекции на ActionScript интерфейса IUserService сгенерированной GraniteDS.
    public function addOneCurr(arg0:mCurriculum, arg1:Number, resultHandler:Object = null, faultHandler:Function = null):void 
    {
      if (faultHandler != null)
        callProperty("addOneCurr", arg0, arg1, resultHandler, faultHandler);
      else if (resultHandler is Function || resultHandler is ITideResponder)
        callProperty("addOneCurr", arg0, arg1, resultHandler);
      else if (resultHandler == null)
        callProperty("addOneCurr", arg0, arg1);
      else
        throw new Error("Illegal argument to remote call (last argument should be
        Function or ITideResponder): " + resultHandler);
    }
    

    Та же функция в исходном java классе UserService, который реализует интерфейс IUserService.
        @Override
        public Boolean addOneCurr(mCurriculum cur,Integer k){
            objT.RevTranformCu(cur,k);
            return true;
        }
    

    Эта функция вызывает метод RevTranformCu(mCurriculum cur, Integer act) класса CacheTransform, предназначенный для преобразования объекта класса mCurriculum в объект класса cCurriculum и его записи в БД. Кроме того, функция addOneCurr «каскадно» вызывает сохранение объектов, входящих в коллекции, обрабатываемого учебного плана.
    Метод RevTranformCu.
    ///Трансформация УП
    public cCurriculum RevTranformCu(mCurriculum cur,Integer act) {
      try {
        cCurriculum res=null;
        if (act==1) {
          System.out.println("//MAS: TRY EDIT Curriculum: " + act.toString() + 
          "\nID:" + cur.getId().toString());
          res=(cCurriculum) cCurriculum._open(dbconnection, new Id(cur.getId()));
        }
        else {
          System.out.println("//MAS: TRY ADD Curriculum");
          res=new cCurriculum(dbconnection);
        }
        res.setName(cur.getName());
        //Коллекция циклов
        if(res.getListOfCicl()!=null) {
          res.getListOfCicl()._clear();
        }
        if(cur.getListOfCicl()!=null) {
          for (int i=0;i<cur.getListOfCicl().size();i++) {
            cur.getListOfCicl().get(i).setCurriculum(cur.getId());
            if(cur.getListOfCicl().get(i).getId()!=null) {
              res.getListOfCicl().add(RevTranformCi
              (cur.getListOfCicl().get(i),1));
            }
            if(cur.getListOfCicl().get(i).getId()==null) {
              res.getListOfCicl().add(RevTranformCi
              (cur.getListOfCicl().get(i),2));
            }
          }
        }
        //Коллекция семестров
        if(res.getListOfSemestr()!=null) {
          res.getListOfSemestr()._clear();
        }
        if(cur.getListOfSemestr()!=null) {
          for (int i=0;i<cur.getListOfSemestr().size();i++) {
            cur.getListOfSemestr().get(i).setCurriculum(cur.getId());
            if(cur.getListOfSemestr().get(i).getId()!=null) {
              res.getListOfSemestr().add(RevTranformSe
              (cur.getListOfSemestr().get(i),1));
            }
            if(cur.getListOfSemestr().get(i).getId()==null) {
              res.getListOfSemestr().add(RevTranformSe
              (cur.getListOfSemestr().get(i),2));
            }
          }
        }
        //Список логов
        if(res.getListOfLogs()!=null) {
          res.getListOfLogs()._clear();
        }
        if(cur.getListOfLogs()!=null) {
          for (int i=0;i<cur.getListOfLogs().size();i++) {
            cur.getListOfLogs().get(i).setCurriculum(cur.getId());
            if(cur.getListOfLogs().get(i).getId()!=null) {
              res.getListOfLogs().add(RevTranformLo
              (cur.getListOfLogs().get(i),1));
            }
            if(cur.getListOfLogs().get(i).getId()==null) {
              res.getListOfLogs().add(RevTranformLo
              (cur.getListOfLogs().get(i),2));
            }
          }
        }
        res._save();
        return res;
      }
      catch (CacheException e) {
        e.printStackTrace();
        return null;
      }
    }
    


    Как видно из примера, метод последовательно выполняет сохранение всех объектов из списков семестров, циклов и логов соответствующими методами RevTranformSe, RevTranformCi, RevTranformLo. При этом принцип остаётся прежним: если id отсутствует, то создаётся новый объект, иначе редактируется существующий.
    Аналогично протекает и удаление учебного плана. Для удаления достаточно получить id объекта класса-проекции mCurriculum и передать его на сервер. За это отвечает функция delOneCurr в AS проекции функции интерфейса IUserService.
    userService.delOneCurr
     (
       curId,
       function (e:TideResultEvent):void 
        {
    	  loadCur(0);
        },
       function (e:TideFaultEvent):void 
        {
          Alert.show(e.fault.faultDetail);
        }
     );
    

    Та же функция в исходном java классе UserService, который реализует интерфейс IUserService.
    @Override
    public void delOneCurr(Integer i) 
     {
       objT.deleteOneCurr(i);
     }
    

    Функция delOneCurr вызывает метод deleteOneCurr(Integer dd) класса CacheTransform, предназначенный для удаления объекта класса cCurriculum из БД. Кроме того, эта функция каскадно вызывает удаление объектов входящих в листы удаляемого учебного плана.
    Метод deleteOneCurr.
    public void deleteOneCurr(Integer dd) {
      try {
        System.out.println("//MAS: TRY DELETE Curriculum");
        cCurriculum cur;
        cur=(cCurriculum) cCurriculum._open(dbconnection, new Id(dd));
        if(cur.getListOfCicl()!=null){
          for(int i=0;i<cur.getListOfCicl().size();i++) {
            cCicl k=(cCicl)cur.getListOfCicl().asList().get(i);
            deleteOneCicl(Integer.parseInt(k.getId().toString()));
          }
        }
        if(cur.getListOfSemestr()!=null) {
          for(int i=0;i<cur.getListOfSemestr().size();i++) {
            cSemestr k = (cSemestr)cur.getListOfSemestr().asList().get(i);
            deleteOneSeme(Integer.parseInt(k.getId().toString()));
          }
        }
        if(cur.getListOfLogs()!=null) {
          for(int i=0;i<cur.getListOfLogs().size();i++) {
            cLogs k=(cLogs)cur.getListOfLogs().asList().get(i);
            deleteOneLog(Integer.parseInt(k.getId().toString()));
          }
        }
        cur._close();
        cCurriculum._deleteId(dbconnection, new Id(dd));
        System.out.println("//MAS: DELETE Complite");
      }
      catch (CacheException e) {
        e.printStackTrace();
      }
    }
    


    Как видно из примера, метод последовательно выполняет удаление всех объектов из списков семестров, циклов и логов соответствующими методами deleteOneSeme, deleteOneCicl, deleteOneLog.

    Несколько слов о «продакшене»


    Так как Cache-проекции в полной мере поддерживают принципы ООП, то добавление поля или изменения типа в Cache-классе равносильно соответствующей операции непосредственно в Java.
    Если говорить конкретнее, то использование проекций и дублирование классов позволяет относительно легко вносить изменения в проект при добавлении нового поля или при изменении типа данных в Cache.
    Такое решение легко поддерживать, главное требование которое необходимо выполнять – соблюдение нотации при именовании классов, свойств и методов.
    При увеличении числа классов, код растет прямо пропорционально, а если постараться унифицировать функции Tranform, то в разы меньше.
    На данном этапе при внесении изменений или же добавлении нового класса необходимо:
    • заново сгенерировать классы-проекции и заменить существующие (или же добавить новые);
    • внести соответствующие изменения в m-классы (так как реализованы принципы ООП, то подобные изменения не составят особого труда);
    • внести изменения в контроллер (при унификации можно избежать).

    Заключение


    Рассматривая все плюсы и минусы представленной комбинации и предложенного подхода, стоит учитывать дальнейшее развитие системы в сторону агентных технологий.
    Поскольку в качестве платформы была выбрана JADE, то естественно, что она накладывает ряд ограничений на проектирование архитектуры всей системы. Так как сама JADE написана на языке Java и является кроссплатформенной системой работающей под виртуальной машиной, то и логика разрабатываемой МАС также строится в этой среде. Основной единицей (элементом) JADE является агент, который, по сути, является объектом java-класса. Вся логика функционирования этого агента также реализуется в этом классе. Долговременное хранение данных каждого агента удобно реализовывать, в объектно-ориентированной базе данных, т.к. сам агент предполагает наличие в нём интеллектуальной составляющей, которая может основываться на семантических сетях и онтологиях. Онтологии в свою очередь являются хранимыми данными и наследуют (если можно так выразиться) объектно-ориентированный подход. Таким образом, самой удобной СУБД для реализации данной системы является объектно-ориентированная и предоставляющая возможность работы с ней через java-классы. СУБД Caché, по нашему мнению, является хорошим кандидатом для решения данной задачи.
    На данном этапе сложно оценить эффективность использования Flex технологии в качестве web-интерфейса, поскольку связка Caché-Java дает достаточно широкий выбор для дальнейшего развития.
    К недостаткам предложенной архитектуры можно отнести дублирование java-проекций в пакете «Java m-classes» и реализацию дополнительных функций в классе CacheTransform, обеспечивающих синхронизацию объектов классов java-проекций и m-classes. В будущем данная проблема может быть решена путём доработки механизма генерации java-проекций в Caché, который сможет учитывать дополнительную специфику, определяемую внешними приложениями, используемыми java-проекции. Например, дополнительные требования к генерации проекций могут быть описаны в отдельном xml-файле, используя который механизм генерации будет создавать правильные java-проекции. Тогда необходимость в дубликатах классов java-проекций отпадёт.

    От авторов


    Надеемся, что вам было интересно читать данную статью, и вы смогли найти что-то полезное для себя. Мы, безусловно, продолжим работу над проектом (в том числе и работу над его оптимизацией), поэтому возможно в дальнейшем данный цикл статей будет расширяться.
    Сейчас же мы рассмотрели лишь один из модулей разрабатываемой мультиагентной системы, и нашей основной целью было ознакомить читателя со связкой Caché + Java + Flex. В то же время мы оставили нераскрытым вопрос об использовании агентов. Поэтому, для тех, кому интересна данная тема, предлагаем ознакомиться с некоторыми пояснениями под спойлером.

    Агент и микроагент
    Данные компоненты проекта необходимы для дальнейшего расширения функционала в соответствии с конкретными требованиями к системе. На текущий момент реализована лишь связь web-приложения с агентом на сервере и вызов ряда его функций. Это в дальнейшем поможет в разработке системы, построенной на взаимодействии агентов в среде JADE, где представленный проект является лишь одним из визуальных модулей агента. Микроагент, по сути, является вспомогательным компонентом для облегчения работы с мультиагентной системой пользователей. Каждому пользователю предоставляется его собственный агент, через который он взаимодействует с другими агентами системы. Для обеспечения постоянной связи агентов с пользователем и используется микроагент, который устанавливается на компьютере пользователя. Микроагент информирует пользователя о происходящих событиях и получаемых сообщениях, передаваемых основным агентом. Он также обеспечивает запуск приложения на клиенте (браузер, отображающий web-страницы), позволяющего реализовать диалог пользователя с его агентом.
    Продемонстрируем функцию, запускающую новый контейнер и агента на платформе JADE при старте web-сервера.
        public boolean StartAgent()
        {
            String aName = "ZavKaf Agent - ";
            String aClass = "agents.ZavCafAgent";
            rt= Runtime.instance();
            p=new ProfileImpl();
            p.setParameter("container-name","ZavKaf_Agent");
            mainCont=rt.createAgentContainer(p);
            try {
                Object[] temp=new Object[1];
                temp[0]=testSoul;
                ac=mainCont.createNewAgent(aName, aClass, temp);
                ac.start();
                agSt=true;
                System.out.println("Agent Start");
                return true;
            }
            catch (Exception ex) {
                testSoul.setA(null);
                System.out.println("Agents ERROR: " + ex);
                return false;
            }
        }
    

    Ниже показан вызов одного из модулей агента отвечающий за функциональное построение учебного плана.
    public mAlgRes doAlgorithm(List<mDiscipline> curL, mCurriculum curriclum, List<mControlForm> cf) {
      return Agents.getA().doAlg(curL,curriclum,cf);
    }
    

    Здесь Agents это объект класса, хранящий в себе данные об активных агентах и агенте, связанным с текущим web-приложением.
    InterSystems
    87,00
    Вендор: СУБД Caché, OLAP DeepSee, шина Ensemble
    Поделиться публикацией

    Похожие публикации

    Комментарии 7

      0
      Добрый день.

      Насколько я помню из прошлых частей, использование прослойки на java, вызвано тем, что вы используете агенты. И тут у меня есть ряд вопросов по поводу технологий.

      Зачем использовать Cach'e? Ведь каше не самая лучшая СУБД. Она показывает всю силу, если вы используете CacheScript и не забываете про глобаллы.

      Насколько оправданно использование агентов? Неужели нельзя было решить задачу на стороне Cach'e? После чего реализовать ряд SOAP сервисов на стороне Cach'e и дергать их из флекса. Благо каше умеет создавать SOAP сервисы, а на стороне флекса можно генерить прокси классы по WSDL.

      Ну и собственно почему именно Cach'e + java + flex? Три больших технологии, которые при разработке надо знать. Более того, Cach'e, не самая распространенная технология на нашем рынке, а flex то уже отмирает. Чем обусловлен этот выбор? Ну кроме того, что если мы используем Cach'e, нас поощряет InterSystems?
        0
        Зачем использовать Cach'e?

        Это представитель объектной СУБД, в которой генерируются проекции на java, что позволяет реализовать объектную связку с java-классами нашей системы. Какие ещё есть кандидаты с таким функционалом?
        Насколько оправданно использование агентов? Неужели нельзя было решить задачу на стороне Cach'e? После чего реализовать ряд SOAP сервисов на стороне Cach'e и дергать их из флекса.

        Тогда нам бы пришлось написать на Cache мультиагентную платформу аналогичную JADE в соответствие со стандартом FIPA. Использование мультиагентной платформы обусловлено сложностью задачи. В комментариях к первой части объясняются причины выбора мультиагентного подхода.
        Ну и собственно почему именно Cach'e + java + flex? Три больших технологии, которые при разработке надо знать. Более того, Cach'e, не самая распространенная технология на нашем рынке, а flex то уже отмирает. Чем обусловлен этот выбор? Ну кроме того, что если мы используем Cach'e, нас поощряет InterSystems?

        Не всегда распространённость технологии говорит о её преимуществах и является критерием её выбора. В нашей организации есть лицензия на Cache и разработаны некоторые системы, также используются и другие технологии и СУБД. Здесь Cache была выбрана из-за её объектности и java-проекций, не считая других её качеств как СУБД.
        Что касается flex, то здесь не всё так однозначно, возможно другие модули системы уже будут реализовываться на HTML5.
          0
          Зачем использовать Cach'e? Ведь каше не самая лучшая СУБД.
          Обычно говорят «СУБД плохая» если не получается сделать что-то определенное, или существует совершенно конретная проблема, которую не удалось решить с помощью СУБД. Посему вопрос — а что не получилось сделать?
          0
          Это представитель объектной СУБД, в которой генерируются проекции на java, что позволяет реализовать объектную связку с java-классами нашей системы.
          Ну не объектной, а постреляционной (или что то изменилось?). В полной мере наследование лучше не использовать. Т.к. оно сделано через одно место. Будет создаваться две сущности (не помню уже как там конкретно в терминах на уровне Cach'e), со связями один к одному. В общем то, что вы видите объектное представление, не есть в полной мере объектное представление на уровне хранения данных. Отсюда следует что используя любую ORM над реляционной СУБД, вы получите ровно тот же функционал. Но раз мы уж говорим о объектах на уровне СУБД, то какая часть функционала вынесена в эти объекты? Так ли вам нужны объекты на уровне СУБД? Или вам будет, опять таки, достаточно объектного представления реляционной модели?
          Тогда нам бы пришлось написать на Cache мультиагентную платформу аналогичную JADE в соответствие со стандартом FIPA.
          Я почитал. Возможно это оправданно. Зачем придумывать костыли, если есть уже готовое решение для вашей задачи.

          Но как я уже говорил, не понятно зачем такой зоопарк технологий. Если уж вам так важно использовать каше, то может лучше реализовать часть функционала (самое необходимое) Jade на стороне Cach'e? Да, это велосипед, который позволяет полностью отказаться от использования лишней технологии! Мне кажется игра стоит свеч! Ведь проще будет погружаться и система станет проще. Но скорее всего, я бы пришел к выводу, что Cach'e есть лишняя технология. Объектное представление данных вам даст любая ORM технология.
            0
            Ну не объектной, а постреляционной (или что то изменилось?).

            Нет, ничего не изменилось, я имел в виду объектное представление данных.
            В полной мере наследование лучше не использовать. Т.к. оно сделано через одно место. Будет создаваться две сущности (не помню уже как там конкретно в терминах на уровне Cach'e), со связями один к одному.

            Создадутся две таблицы в реляционном представлении. Но все данные хранятся в разреженных многомерных массивах. И в случае создания и сохранения объектов класса наследника, в БД создастся один глобал, а не два, но в реляционном виде будет две таблице, в одной из которых появится несколько записей по числу сохранённых объектов.
            Но раз мы уж говорим о объектах на уровне СУБД, то какая часть функционала вынесена в эти объекты? Так ли вам нужны объекты на уровне СУБД? Или вам будет, опять таки, достаточно объектного представления реляционной модели?

            Некоторый функционал по обработке данных у нас вынесен на уровень классов Cache, что конечно же не делает это панацеей. Безусловно, можно было бы использовать ORM, ровно как его и не использовать, и работать напрямую с БД, будь она реляционной.
            Я почитал. Возможно это оправданно. Зачем придумывать костыли, если есть уже готовое решение для вашей задачи.

            О каком готовом решении для нашей задачи идёт речь?
            Если уж вам так важно использовать каше, то может лучше реализовать часть функционала (самое необходимое) Jade на стороне Cach'e?

            Может в этом и есть какой-то смысл, но на это точно нет ресурсов.
            Но скорее всего, я бы пришел к выводу, что Cach'e есть лишняя технология. Объектное представление данных вам даст любая ORM технология

            А любая другая реляционная СУБД с ORM надстройкой лишней технологией уже являться не будет?
              0
              О каком готовом решении для нашей задачи идёт речь?
              Я имел ввиду JADE.

              А любая другая реляционная СУБД с ORM надстройкой лишней технологией уже являться не будет?
              Да, наверное я не верно употребил термины. Не технология, а платформа лишняя. Cach'e или java мне кажутся лишними. ORM будет новой технологией, но не платформой. Просто это совсем другой уровень. ORM будет написан на java и будет использовать примочки именно этой платформы. СУБД как платформу я думаю рассматривать глупо, не будете же вы туда логику выносить, в процедуры. У вас же получается используются три платформы, для решения задачи. Когда достаточно двух. А то и одной джавы.

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

              А чем не нравится объектная модель в Cache'?
              И что не так с наследованием. Что я делаю не так, что не вижу проблем?

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое