Функция клас срещу фабрика: проучване на пътя напред

Открийте Функционалния JavaScript беше обявен за една от най-добрите нови книги за функционално програмиране от BookAuthority!

ECMAScript 2015 (известен още като ES6) идва със синтаксиса на класа, така че сега имаме два конкуриращи се модела за създаване на обекти. За да ги сравня, ще създам същата дефиниция на обект (TodoModel) като клас и след това като фабрична функция.

TodoModel като клас

клас TodoModel {
    конструктор () {
        this.todos = [];
        this.lastChange = null;
    }
    
    addToPrivateList () {
        console.log ( "addToPrivateList");
    }
    add () {console.log ("добавяне"); }
    презареди () {}
}

TodoModel като фабрична функция

функция TodoModel () {
    var todos = [];
    var lastChange = null;
        
    функция addToPrivateList () {
        console.log ( "addToPrivateList");
    }
    функция add () {console.log ("добавяне"); }
    функция reload () {}
    
    върнете Object.freeze ({
        добави,
        презареди
    });
}

Капсулирането

Първото нещо, което забелязваме, е, че всички членове, полета и методи на клас обект са публични.

var todoModel = нов TodoModel ();
console.log (todoModel.todos); // []
console.log (todoModel.lastChange) // null
todoModel.addToPrivateList (); // addToPrivateList

Липсата на капсулация може да създаде проблеми със сигурността. Вземете примера за глобален обект, който може да бъде променен директно от конзолата за програмисти.

Когато използвате фабрична функция, само методите, които излагаме, са публични, всичко останало се капсулира.

var todoModel = TodoModel ();
console.log (todoModel.todos); // неопределен
console.log (todoModel.lastChange) // неопределен
todoModel.addToPrivateList (); //taskModel.addToPrivateList
                                    не е функция

това

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

клас TodoModel {
    конструктор () {
        this.todos = [];
    }
    
    презареди () {
        setTimeout (функция лог () {
           console.log (this.todos); // неопределен
        }, 0);
    }
}
todoModel.reload (); // неопределен

или това губи контекст, когато методът се използва като обратно извикване, например при DOM събитие.

. $ ( "# BTN") щракнете върху (todoModel.reload); // неопределен

Няма такива проблеми при използване на фабрична функция, тъй като тя изобщо не използва това.

функция TodoModel () {
    var todos = [];
        
    функция reload () {
        setTimeout (функция лог () {
           console.log (Todos); // []
       }, 0);
    }
}
todoModel.reload (); // []
. $ ( "# BTN") щракнете върху (todoModel.reload); // []

тази функция и стрелка

Функцията стрелка частично решава проблемите на този губещ контекст в класовете, но в същото време създава нов проблем:

  • това вече не губи контекст в вложени функции
  • това е губещ контекст, когато методът се използва като обратно извикване
  • стрелката функция насърчава използването на анонимни функции

Реконструирах отново TodoModel с помощта на стрелката. Важно е да се отбележи, че в процеса на рефакторинг към функцията със стрелки можем да загубим нещо много важно за четимост, името на функцията. Вижте например на:

// използване на име на функция за изразяване на намерение
setTimeout (функция renderTodosForReview () {
      / * код * /
}, 0);
// срещу използване на анонимна функция
setTimeout (() => {
      / * код * /
}, 0);

Неизменяем API

След като обектът е създаден, очаквам неговият API да бъде неизменен. Лесно мога да променя изпълнението на публичен метод, за да направя нещо друго, когато той е създаден с помощта на клас.

todoModel.reload = функция () {console.log ("ново презареждане"); }
todoModel.reload (); // ново презареждане

Този проблем може да бъде решен чрез извикване на Object.freeze (TodoModel.prototype) след дефиницията на класа.

API на обекта, създаден с помощта на фабрична функция, е неизменна. Забележете използването на Object.freeze () на върнатия обект, съдържащ само публичните методи. Личните данни на обекта могат да бъдат променяни, но само чрез тези публични методи.

todoModel.reload = функция () {console.log ("ново презареждане"); }
todoModel.reload (); // презареждане

нов

new трябва да се използва при създаване на обекти с помощта на класове.

ново не се изисква при създаване на обекти с фабрични функции, но ако това го направи по-четимо, можете да продължите за него, няма вреда.

var todoModel = нов TodoModel ();

Използването на ново с фабрична функция просто ще върне обекта, създаден от фабриката.

Състав над наследството

Класовете поддържат както наследяване, така и състав.

По-долу е даден пример за наследяване, при което клас SpecialService наследява от Service class:

клас услуга {
  doSomething () {console.log ("doSomething"); }
}
клас SpecialService разширява услугата {
  doSomethingElse () {console.log ("doSomethingElse"); }
}
var specialService = нов SpecialService ();
specialService.doSomething ();
specialService.doSomethingElse ();

Ето още един пример, при който SpecialService използва повторно член на услугата, използвайки състав:

клас услуга {
  doSomething () {console.log ("doSomething"); }
}
клас SpecialService {
  конструктори (аргументи) {
    this.service = args.service;
  }
  doSomething () {this.service.doSomething (); }
  
  doSomethingElse () {console.log ("doSomethingElse"); }
}
var specialService = нов SpecialService ({
   услуга: нова Услуга ()
});
specialService.doSomething ();
specialService.doSomethingElse ();

Фабричните функции насърчават състава над наследяването. Разгледайте следващия пример, в който SpecialService използва повторно членовете на Услугата:

функция Услуга () {
  функция doSomething () {console.log ("doSomething"); }
  върнете Object.freeze ({
    направи нещо
  });
}
функция SpecialService (args) {
  var service = args.service;
  функция doSomethingElse () {console.log ("doSomethingElse"); }
  върнете Object.freeze ({
    doSomething: service.doSomething,
    doSomethingElse
  });
}
var specialService = SpecialService ({
   услуга: Сервиз ()
});
specialService.doSomething ();
specialService.doSomethingElse ();

памет

Класовете са по-добри при запазване на паметта, тъй като се реализират над прототипната система. Всички методи ще бъдат създадени само веднъж в прототипния обект и споделяни от всички инстанции.

Цената на паметта на фабричната функция е забележима при създаването на хиляди от един и същ обект.

Ето страницата, използвана за тестване на цената на паметта при използване на фабричната функция.

Цената на паметта (в Chrome)
+ ----------- + ------------ + ------------ +
| Примери | 10 метода | 20 метода |
+ ----------- + --------------- + --------- +
| 10 | 0 | 0 |
| 100 | 0.1Mb ​​| 0.1Mb ​​|
| 1000 | 0.7Mb | 1.4Mb |
| 10000 | 7.3Mb | 14.2Mb |
+ ----------- + ------------ + ------------ +

Обекти срещу структури от данни

Преди да анализирате допълнително цената на паметта, трябва да се направи разграничение между два вида обекти:

  • OOP обекти
  • Обекти на данни (известни още като структури от данни)
Обектите излагат поведение и крият данни.
Структурите на данните излагат данни и нямат значително поведение.
- Робърт Мартин „Чист код“

Ще разгледам отново примера на TodoModel и ще обясня тези два вида обекти.

функция TodoModel () {
    var todos = [];
           
    функция add () {}
    функция reload () {}
       
    върнете Object.freeze ({
        добави,
        презареди
    });
}
  • TodoModel е отговорен за съхраняването и управлението на списъка на todos. TodoModel е обект OOP, който излага поведение и крие данни. В приложението ще има само един екземпляр от него, така че няма допълнителни разходи за памет при използване на фабричната функция.
  • Обектите todos представляват структурите на данните. Може да има много от тези обекти, но те са просто обикновени JavaScript обекти. Не сме заинтересовани да поддържаме техните методи частни - по-скоро искаме да изложим всичките им данни и методи. Така че всички тези обекти ще бъдат изградени над прототипната система и те ще се възползват от запазването на паметта. Те могат да бъдат изградени с помощта на обикновен обект буквално или Object.create ().

UI компоненти

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

Компонентите ще бъдат изградени в съответствие с рамковата практика на компонентите. Например, обектните литерали ще бъдат използвани за Vue или класове за React. Членовете на всеки компонент ще бъдат публични, но те ще се възползват от запазването на паметта на прототипната система.

заключение

Силните страни на класа са нейното познаване на хората, идващи от класа, базиран на класа, и по-хубавият му синтаксис над прототипната система. Въпреки това, проблемите му със сигурността и използването на този, непрекъснат източник на загуба на контекстни грешки, го прави втори вариант. Като изключение класовете ще се използват, ако това се изисква от рамката на компонента, както в случая на React.

Фабричната функция е не само по-добрият вариант за създаване на защитени, капсулирани и гъвкави OOP обекти, но също така отваря вратата за нова, уникална за JavaScript, парадигма за програмиране.

Открийте Функционалния JavaScript беше обявен за една от най-добрите нови книги за функционално програмиране от BookAuthority!

За повече информация относно прилагането на техники за функционално програмиране в React, разгледайте Functional React.

Прочетете повече за Vue и Vuex в Бързо въведение към Vue.js Components.

Научете как да прилагате принципите на моделите за дизайн.

Можете да ме намерите и в Twitter.