Предисловие


Не секрет, что современный разработчик старается повысить эффективность и призывает себе на помощь библиотеки и каркасы.


Слово framework(каракас) настолько вошло в обиход, что стала встречаться путаница — что можно назвать каркасом, а что таковым не является?


Эта работа имеет цель прояснить особенности, отличия каркаса от библиотеки. Наверно есть случаи, когда вообще тяжело определить что перед нами каркас или библиотека, так как каркас может нести с собой набор вспомогательных библиотек.


Библиотека


С библиотекой все просто. Кто-то написал код, выставил наружу открытые методы/свойства(API) и этим можно пользоваться. В некотором смысле библиотека сервис, а ваш код клиент.


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


Делаем вывод, что библиотека накладывает на нас ограничения на уровне реализации, этапе конструирования(кодирования) нашего приложения.


В качестве примера приведем фрагмент воображаемой JavaScript библиотеки работы с именами и фамилиями:


var nameUtil = {
        correctFullName:function(fullname){
            var f = fullname.replace(/^ +| +$| {2}/g, "")
            f =  f.substring(0,1).toUpperCase()+f.substring(1,f.length)
            f = f.substring(0, f.indexOf(" ")+1)
                + (f.substring(f.indexOf(" ")+1, f.indexOf(" ")+2)).toUpperCase()
                + f.substring(f.indexOf(" ")+2, f.length)
            return f
        }
}

Метод “nameUtil.correctFullName” исправляет написание полного имени человека, то есть “ John leaf” исправит в “John Leaf”. Мы помещаем эту библиотеку в проект и просто начинаем пользоваться ее сервисами — вызываем ее методы, то есть:


nameUtil.correctFullName(“some Name”)

Каркас


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


Статья на на Wikipedia, ссылаясь на труд Вольфганга Прии, упоминает о замороженных и горячих точках. В самом источнике “Meta Patterns—A Means For Capturing the Essentials of Reusable Object-Oriented Design” говорится о горячих точках, серых и белых.


Попробуем проинтерпретировать это следующей иллюстрацией:



Рис. 1 — Каркас и остальные части системы


Голубые точки показывают части каркаса, они неизменны. Оранжевые точки это те части, которые было созданы в рамках какого-либо проекта.


Чаще всего упоминают следующие отличия между библиотекой и каркасом:


  • вы вызываете код библиотеки
  • каркас вызывает ваш код

Практический пример каркаса


Создадим каркас и посмотрим, как нам удалось реализовать свойства каркаса. Каркас реализует архитектурный шаблон, в нашем случае это MVC. В части обработки событий применен шаблон проектирования “Издатель-подпсчик”.


С помощью каркаса напишем часть воображаемого приложения управления персоналом. Наш фрагмент приложения будет только отображать список работников в таблице с возможностью фильтрации по стажу.


Напишем для нашей системы следующие компоненты на JavaScript:


EventBus – объект-экземпляр контроллера, шина событий. В ней мы регистрируем подписчиков событий и размещаем события. При размещении события шина находит подписчиков по сигнатуре события(идентификатору) и вызывает метод обработки события. Шина событий связывает все части нашей системы. В системе существует в единственном экземпляре, поэтому создадим его с помощью литерной нотации JavaScript.


/*
 * Объект-экземпляр, шина событий, позволяет подписываться на события и публиковать события.
 */
EventBus = {
    subscribers:[],
    /*
     * регистрирует подписчика события
     * @param subscriberObject объект-экземпляр подписчика события
     * @param subscriberMethod метод  подписчика события для обработки сообщения
     */ 
    subscribe:function(subscriberObject, subscriberMethod, event) {
        var subscriber = {}
        subscriber.object = subscriberObject
        subscriber.method = subscriberMethod
        subscriber.event = event 
        this.subscribers[this.subscribers.length]=subscriber
    },
    /*
     * публикация события
     * @param eventId строка-идетификатор события
     * @param params параметры
     * @param callback метод, который будет вызван для возвращения результата отработки подписчика события
     * @return возвращает результат отработки по событию 
     */
    publish:function(eventId, params, callback){
        for(var i=0; i<this.subscribers.length;  i++){          
            var event = this.subscribers[i].event
            //console.log("eventId=",eventId, " callback=", typeof callback)
            if(event===eventId){
                if(typeof callback=='function'){
                    callback(this.subscribers[i].object[this.subscribers[i].method](params))
                } else {
                    this.subscribers[i].object[this.subscribers[i].method](params)
                }
            }           
        }
    }
}

Table — компонент визуального отображения информации в виде таблицы. Для него нужна структура данных определенного вида. Реализован как объект-конструктор, можно создавать экземпляры.


/*
 * Объект-конструктор, визуальный элемент показывающий таблицу
 * @param nameParam уникальное имя таблицы
 * @param eBus шина событий
 * @param domId идентификатор узла DOM-дерева для размещения таблицы
 */
function Table(nameParam, eBus, domId){
    var name = nameParam
    var targetDomId = domId
    var eventBus = eBus
    /*
     * Показывает таблицу
     * @param params  параметры отображения, передаются источнику данных для таблицы
     */
    this.show = function(params){
        var data={}
        data.headers=[]
        data.rows=[]
        this.remove()
        // get data from a data source              
        eventBus.publish(name+ ".getDataSet", params,  function(d){ data = d})
        var targetDom = document.getElementById(targetDomId)
        var tableElement = document.createElement("TABLE")
        var tbodyElement = document.createElement("TBODY")
        var trElement = document.createElement("TR")
        trElement.style.background="#eaeaea"
        for(i=0;i<data.headers.length;i++){
            var tdElement = document.createElement("TD")            
            tdElement.appendChild(document.createTextNode(data.headers[i]))
            trElement.appendChild(tdElement)
        }
        tbodyElement.appendChild(trElement)

        for(i=0;i<data.rows.length;i++){
            var trElement = document.createElement("TR")
            for(j=0;j<data.rows[i].length; j++){
                var tdElement = document.createElement("TD")            
                tdElement.appendChild(document.createTextNode(data.rows[i][j]))
                trElement.appendChild(tdElement)
            }
            tbodyElement.appendChild(trElement)
        }

        tableElement.appendChild(tbodyElement)
        targetDom.appendChild(tableElement)     
        tableElement.border=1
    }
    /*
     * удаляет таблицу из DOM 
     */
    this.remove = function(){
        var targetDom = document.getElementById(targetDomId)
        try{
            while (targetDom.firstChild) {
                targetDom.removeChild(targetDom.firstChild);
            }
        }catch(e){}
    }
}

DataSource — источник данных, объект-экземпляр. Он умеет отдавать определенную структуру данных. Внутри содержит тестовый набор самих данных (переменная data).


/*
 * Объект-экземпляр, источник данных
 */
DataSource = {
    /*
     * регистрирует подписчика события
     * @return  возвращает данные
     */
    loadDepartments:function(){
        return data        
    }
}
// структура данных
var data = [
            {name:"IT",
            employees:[
                       {name:"Federico", surname:"Gonsales", position:"Engineer", hirenDate:"2013-01-02"},
                       {name:"Mike", surname:"Saldan", position:"Tester", hirenDate:"2011-11-22"},
                       {name:"Leo", surname:"Sigh", position:"Architect", hirenDate:"2001-12-12"}
                       ]
            },
            {name:"Sales",
                employees:[
                           {name:"Sarah", surname:"Connor", position:"Manager", hirenDate:"2010-04-14"},
                           {name:"Richard", surname:"Senom", position:"Specialist", hirenDate:"2014-05-07"}                                 
                           ]
                }
       ]

Employee — объект-конструктор модели, представляющий объект предметной области — Сотрудника.


function Employee(nameParam, surnameParam, positionParam, hirenDateParam){
    var name      = nameParam
    var surname = surnameParam  
    var position   = positionParam
    var hirenDate = hirenDateParam
    this.setName = function(n){
        name=n
    }
    this.setSurname = function(s){
        surname=s
    }
    this.setPosition = function(p){
        position=p
    }
    this.setHirenDate = function(d){
        hirenDate=d
    }
    this.getName = function(){
        return name
    }
    this.getSurname = function(){
        return surname
    }
    this.getPosition = function(){
        return position
    }
    this.getHirenDate = function(){
        return hirenDate
    }
    this.getFullName = function(){
        return name+" "+surname
    }
    this.getExperience = function(){
        var oneDay = 24*60*60*1000;
        var now = new Date();
        //console.log(hirenDate.getTime() +"-"+ now.getTime())
        var diffDays = Math.round(Math.abs((hirenDate.getTime() - now.getTime())/(oneDay)))
        return (diffDays/365).toFixed(0)
    }
}

Department — объект-конструктор модели, представляющий объект предметной области — Отдел.


/*
 * Объект-конструктор отдел, содержит сведения о отделе
 * @param nameParam  название отдела
 */
function Department(nameParam){
    var name = nameParam
    var employees = []
    this.setName = function(n){
        name=n
    }
    this.addEmployee = function(e){
        employees[employees.length] = e
    }
    this.getName = function(){
        return name
    }    
    this.getEmployees = function(){        
        return employees
    }
}

Main — главный объект приложения, соединяет все вместе. В части использования каркас предъявляет свои правила. Для размещения на странице нашей таблицы (Table) нужно выполнить следующие шаги:


Создать экземпляр Table указав обработчик событий, порождаемых визуальным компонентом.
Обработчик событий должен в ответ на вызов с параметром “getDataSet” вернуть JSON-структуру вида:
[headers:[“first”, “second”], rows:[ [value1, value2], [value3, value4] ] ]


Посмотрев объект “Main” может показаться, что нам не нужно создавать экземпляры модели для того, чтоб отправить набор данных компоненту “Table”, но это не так. Объекты модели содержат бизнес-логику, которая не должна быть в Контроллере, например, вычисление стажа работника(метод “getExpirience”)



/*
 * Объект-экземпляр, производит первичную инициализацию, получает данные от источника данных,
 * создает объекты модели, на основе опроса модели формирует набор данных для визуального компонета Table
 */
var Main  = {
    /*
     * создает экземпляр визуального компонента, производит его настройку
     * связывает таблицу, себя и источник данных через шину событий для обмена сообщениями
     */
    init:function(){
        var myTable = new Table("myTable", EventBus, "employeeTable")
        EventBus.subscribe(myTable, "show", 'employee.showTable')
        EventBus.subscribe(this, "getEmployeesDataSet", "myTable.getDataSet")
        EventBus.subscribe(DataSource, "loadDepartments", "loadDepartments")
    },
    /*
     * получает данные от источника, воссоздает модель предметной области, формирует набор данных
     * @param params параметры для источника данных
     */
    getEmployeesDataSet:function(params){
        var data
        EventBus.publish("loadDepartments", null,  function(d){ data = d })
        var departments = []
        for(i=0; i<data.length; i++){
            department = new Department(data[i].name)
                for(j=0;j<data[i].employees.length; j++){
                    var h = data[i].employees[j].hirenDate
                    var hirenDate = new Date(h.substring(0,4), h.substring(5,7), h.substring(8,10))
                    employee = new Employee(data[i].employees[j].name,
                            data[i].employees[j].surname,
                            data[i].employees[j].position,
                            hirenDate)
                    department.addEmployee(employee)
                }
            departments[i] = department 
        }
        records = []
        var c=0
        for(i=0;i<departments.length;i++){          
            department = departments[i]
            employees = department.getEmployees()
            for(j=0;j<employees.length;j++){
                if(params){                 
                    if(params.expirience){
                        if(employees[j].getExperience()*1 >= params.expirience*1){
                            records[c] = [employees[j].getFullName(), employees[j].getPosition(), employees[j].getExperience(), department.getName()]
                            c++
                        }
                    }                   
                }else{
                    records[c] = [employees[j].getFullName(), employees[j].getPosition(), employees[j].getExperience(),  department.getName()]
                    c++
                }
            }
        }
        var dataSet={
                headers:["Full name", "Position",  "Expirience", "Department"],
                rows:records
            }
        return dataSet
    }
}

На рисунке 2 попробуем показать принадлежность классов частям шаблона MVC.



Рис. 2 — Части приложения


На рисунке 3 покажем последовательность обмена сообщениями.



Рис. 3 — Диаграмма последовательности


Итоги


Условно назовем то, что создано “набор кода”.
“Набор кода” не обязывает создавать объекты-конструкторы модели предметной области. В Main.js источником данных для Table.js может быть что угодно. К каркасу, его реализации архитектуры (MVC) это не относится. Мы просто вызываем метод Table.js с определенным параметром. Это свойство библитеки.


“Набор кода” обязывает при использовании визуального компонента Table.js давать ему данные определенной структуры. Это не требование MVC, следовательно к каркасу отношения не имеет. Это свойство библиотеки.


“Набор кода” обязывает визуальные компоненты отправлять свои события по шине событий и чтобы был “слушатель” — подписчик этого события. Представление отделяем от обработки событий, управления, это уже элемент MVC. Это свойство каркаса. Код каркаса (EventBus.js) будет вызывать наш код. Это свойство каркаса.


Выводы


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


Если вы замечаете, что написанный вами или используемый компонент:


а) привносит в проект какой-либо архитектурный шаблон
б) компоненту передается управление ключевых “потоков” выполнения
в) компонент требует от вас некоторой организации ваших частей приложения, то вы имеете дело с каркасом.


Избавиться от каркаса в проекте гораздо сложнее, чем от библиотеки. Библиотеку можно заменить, исправив API-вызовы старой библиотеки на вызовы новой или самим написать реализацию библиотеки. Каркас принуждает строить приложение определенным образом, организовывать, связывать структурные единицы кода по определенным правилам. К тому же, каркасы обычно гораздо сложнее устроены, чем библиотеки.


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


В идеале, хорошо, чтоб реализацию каркаса можно было легко менять. Пока к этому приближается мир Java, в котором благодаря спецификациям задается стандарт, его API, а различные производители могут его реализовывать.


> Исходный код полностью

Поделиться с друзьями
-->

Комментарии (23)


  1. Rastishka
    23.11.2016 18:49
    +12

    каркас
    каркас
    каркас

    Слово "фреймворк" уже давно стало устоявшимся термином, чтобы его использовать без дурацкого перевода.


    1. vyatsek
      24.11.2016 12:20
      -4

      А мне нравится каркас, что за пренебрежение к русскому? либо каркас либо framework.


      1. zesetup
        24.11.2016 21:42

        импортозамещение же


  1. js605451
    23.11.2016 19:16
    +4

    Какая-то длинная статья. Чтобы понять фреймворк это или библиотека, достаточно ответить на вопрос "управляет ли оно потоком выполнения моей программы?". Если да, то это фреймворк, если нет, то это библиотека.


    Если вопрос кажется дурацким или непонятным, его можно переформулировать вот так: "в своём коде я отвечаю на вопросы или скорее делаю утверждения?". Первое — использование фреймворка, второе — использование библиотеки.


    1. zesetup
      23.11.2016 19:23
      -1

      Подробно разобраться хотелось, со своим «велосипедом».
      На ваш взгляд ORM это фреймворк или библиотека?
      Где в .net framework собственно сам фреймворк?


      1. KlimovDm
        23.11.2016 22:54

        На ваш взгляд ORM это фреймворк или библиотека?
        Ни то, ни другое. ORM — это технология.


        1. zesetup
          23.11.2016 23:32

          а в контексте построения ПО?


          1. KlimovDm
            24.11.2016 06:09

            Это уже зависит от конкретной реализации. Мне понравилось, как indestructable сформулировал тему ниже. Если отталкиваться от этого — то библиотека. Ну, например, Doctrine — набор библиотек. При этом Doctrine интегрирована во множество фреймворков.


        1. indestructable
          24.11.2016 00:26

          Хороший ОРМ (для дотнета, например, linq2db, бывший bl toolkit) — библиотека, плохой — фреймворк (пример — первые версии entity framework, linq 2 sql).


          1. zesetup
            25.11.2016 13:34

            как-то странно получается, что в одном случае это фреймворк, а в другом библиотека. попробуем порассуждать: ORM реализует шаблон Data Mapper, но только в слое доступа к данным и заставляет нас строить эту часть приложения определенным образом, основным "потоком" выполнения приложения он не управляет, в таком случае в применении к всему приложению это библиотека, а в слое доступа данных — фреймворк.


            1. indestructable
              27.11.2016 01:59

              Если принять, что фреймворк влияет в том числе на дизайн приложения, то, например, в первых версиях Entity Framework нужно было наследоваться от классов фреймворка, использовать только сгенерированный код, использовать edmx, использовать unit of work. В смысле ограничений весьма сильно.


      1. indestructable
        24.11.2016 00:32
        -1

        В дотнете фреймворк — это джит-компилятор и рантайм. Остальное — это библиотеки.


        1. zesetup
          24.11.2016 01:27

          Какой же это фреймворк. То, как оно там внутри работает не влияет на то, как проектируется приложение.


    1. OnYourLips
      23.11.2016 20:01

      На самом деле всё сложнее, и чёткого определения или даже границы нет.
      Например Symfony — это и фреймворк, и набор библиотек одновременно. И всё зависит от подхода, который выберет разработчик при работе с ним.


    1. indestructable
      24.11.2016 00:27
      +1

      Фреймворк вызывает ваш код, ваш код вызывает библиотеки.


      1. VolCh
        24.11.2016 13:46

        Библиотеки с коллбэками?


        1. justboris
          24.11.2016 13:55
          +1

          При вызове библиотеки и передаче коллбека, инициатива все равно была за вами.


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


          1. laphroaig
            25.11.2016 11:27

            Иными словами библиотеку имеете вы, а фреймворк имеет вас


          1. VolCh
            26.11.2016 07:32

            Ну, есть фреймворки, которые требуют написать определённый код для своего старта.


            1. justboris
              26.11.2016 13:41

              А можете привести пример?
              Без него сложно вам как-то ответить


              1. VolCh
                27.11.2016 07:58

                Первое что под руку попалось:

                require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
                
                $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
                sfContext::createInstance($configuration)->dispatch();
                

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


  1. fsou11
    25.11.2016 11:17

    Бедные подписчики ваших издателей, только один из них получит сообщение о событии, так еще и заставят данные вернуть.

    Вы сами придумали такую интерпритацию паттерна “подписчик/издатель”?


    1. zesetup
      25.11.2016 13:48

      да, это ошибка, должны все получать, исправлю