{"id":299904,"date":"2020-03-10T09:00:24","date_gmt":"2020-03-10T09:00:24","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=299904"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=299904","title":{"rendered":"\u0424\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b\u0439 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434"},"content":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/491684\/\">GoF, \u0427\u0438\u0441\u0442\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430, \u0421\u043e\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u2014 \u043d\u0430\u0441\u0442\u043e\u043b\u044c\u043d\u044b\u0435 \u043a\u043d\u0438\u0433\u0438 \u00abtrue \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u0430\u00bb. \u041d\u043e \u0432 \u043c\u0438\u0440\u0435 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430 \u043c\u043d\u043e\u0433\u0438\u0435 \u0438\u0434\u0435\u0438 \u0438\u0437 \u044d\u0442\u0438\u0445 \u043a\u043d\u0438\u0433 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b. \u041f\u043e \u043a\u0440\u0430\u0439\u043d\u0435\u0439 \u043c\u0435\u0440\u0435 \u0441\u0445\u043e\u0434\u0441\u0442\u0432\u043e \u0441 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c \u043c\u0438\u0440\u043e\u043c \u043d\u0430\u0439\u0442\u0438 \u043e\u0447\u0435\u043d\u044c \u0441\u043b\u043e\u0436\u043d\u043e. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434 \u043e\u043f\u0435\u0440\u0435\u0436\u0430\u0435\u0442 \u0432\u0440\u0435\u043c\u044f? \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u00ab\u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0435\u00bb \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0420\u0435\u0430\u043a\u0442 \u0443\u0436\u0435 \u0434\u043e\u043a\u0430\u0437\u0430\u043b\u0438 \u0441\u0432\u043e\u0435 \u043f\u0440\u0435\u0432\u043e\u0441\u0445\u043e\u0434\u0441\u0442\u0432\u043e \u043d\u0430\u0434 \u041e\u041e\u041f? \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0445\u043e\u0447\u0443 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043f\u0440\u0438\u043c\u0435\u0440 <a href=\"https:\/\/github.com\/justerest\/todo-list\" rel=\"nofollow\">todo-list<\/a> \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u044f \u043f\u043e\u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430\u043c \u0438 \u043f\u043e\u0434\u0445\u043e\u0434\u0430\u043c, \u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u043c \u0432 \u043a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043a\u043d\u0438\u0433\u0430\u0445.<br \/>  <a name=\"habracut\"><\/a>  <\/p>\n<h2>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u043e\u0442 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430<\/h2>\n<p>  \u0424\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u2014 \u043a\u0440\u0430\u0435\u0443\u0433\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u0430\u043c\u0435\u043d\u044c \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0444\u0440\u043e\u043d\u0442\u0430. \u041d\u0430 hh.ru \u0432\u0430\u043a\u0430\u043d\u0441\u0438\u0438 React vs Angular vs Vue \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432. \u042f \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0441 \u043a\u0430\u0436\u0434\u044b\u043c \u0438\u0445 \u044d\u0442\u0438\u0445 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u0432, \u0438 \u043e\u0447\u0435\u043d\u044c \u0434\u043e\u043b\u0433\u043e \u043d\u0435 \u043c\u043e\u0433 \u043f\u043e\u043d\u044f\u0442\u044c, \u0437\u0430\u0447\u0435\u043c \u043c\u043d\u0435 \u043e\u043f\u044b\u0442 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Vue \u043e\u0442 3-\u0445 \u043b\u0435\u0442, \u0447\u0442\u043e\u0431\u044b \u043f\u0435\u0440\u0435\u043a\u0440\u0430\u0441\u0438\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u0438\u0437 \u043a\u0440\u0430\u0441\u043d\u043e\u0433\u043e \u0432 \u0444\u0438\u043e\u043b\u0435\u0442\u043e\u0432\u044b\u0439? \u0417\u0430\u0447\u0435\u043c \u043c\u043d\u0435 \u0437\u043d\u0430\u0442\u044c, \u043a\u0430\u043a \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u043f\u0440\u043e\u0442\u043e\u0442\u0438\u043f\u0430\u0445, \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u0440\u0430\u0431\u043e\u0442\u044b event loop, \u0447\u0442\u043e\u0431\u044b \u043f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u0436\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u0438\u0437 \u043b\u0435\u0432\u043e\u0433\u043e \u0443\u0433\u043b\u0430 \u0432 \u043f\u0440\u0430\u0432\u044b\u0439? \u041e\u0442\u0432\u0435\u0442 \u043f\u0440\u043e\u0441\u0442 \u2014 \u043c\u044b \u043f\u0438\u0448\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u043a \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u043c.<\/p>\n<p>  \u0417\u0430\u0447\u0435\u043c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f\u043c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u0441 \u0431\u043e\u043b\u044c\u0448\u0438\u043c \u0441\u0442\u0430\u0436\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0420\u0435\u0430\u043a\u0442? \u0414\u0430 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044c\u043d\u043e \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0435\u0439 \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u043c\u043e\u0433\u043e \u0420\u0435\u0430\u043a\u0442\u0430, \u0438 \u0447\u0442\u043e\u0431\u044b \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u043f\u043e\u043b\u043e\u043c\u0430\u0442\u044c \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u043a\u0440\u0430\u0448\u0438\u0432\u0430\u043d\u0438\u0438 \u043a\u043d\u043e\u043f\u043a\u0438, \u0432\u0430\u043c \u0441\u0442\u043e\u0438\u0442 \u043f\u043e\u043b\u043e\u043c\u0430\u0442\u044c \u0433\u043e\u043b\u043e\u0432\u0443 \u043d\u0430\u0434 \u0442\u0435\u043c, \u043a\u0430\u043a \u0432\u043d\u0443\u0442\u0440\u0438 \u0420\u0435\u0430\u043a\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 change detection, \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u043d\u0433 \u0434\u0435\u0440\u0435\u0432\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432, \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0437\u0430\u0432\u044f\u0437\u0430\u043d\u043e \u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 \u043f\u0435\u0440\u0435\u043a\u0440\u0430\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u043a\u043d\u043e\u043f\u043a\u0438. (\u0421\u043e\u0433\u043b\u0430\u0441\u0435\u043d, \u044d\u0442\u043e \u0432\u0441\u0451 \u0447\u0430\u0441\u0442\u043d\u044b\u0435 \u0441\u043b\u0443\u0447\u0430\u0438\u2026 \u0410 \u0432 \u0432\u0430\u0448\u0435\u0439 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0433\u043e\u0442\u043e\u0432\u044b \u0432\u0437\u044f\u0442\u044c \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u0438\u0441\u0442\u0430 \u0431\u0435\u0437 \u043e\u043f\u044b\u0442\u0430 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u043c?)  <\/p>\n<blockquote><p>\u041f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u0443\u0439\u0442\u0435 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u044f\u0437\u044b\u043a\u0430, \u0430 \u043d\u0435 \u043d\u0430 \u044f\u0437\u044b\u043a\u0435. (\u041c\u0430\u043a\u043a\u043e\u043d\u0435\u043b\u043b)<br \/>  \u0424\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u2014 \u044d\u0442\u043e \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442, \u0430 \u043d\u0435 \u043e\u0431\u0440\u0430\u0437 \u0436\u0438\u0437\u043d\u0438. (\u041c\u0430\u0440\u0442\u0438\u043d)  <\/p><\/blockquote>\n<p> \u0414\u043b\u044f \u043c\u0438\u0440\u0430 \u0444\u0440\u043e\u043d\u0442\u0430 \u044d\u0442\u0438 \u0442\u0435\u0437\u0438\u0441\u044b \u0432 \u043b\u0443\u0447\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u0443\u0441\u0442\u043e\u0439 \u0437\u0432\u0443\u043a, \u0432 \u0445\u0443\u0434\u0448\u0435\u043c \u2014 \u0432\u044b\u0437\u043e\u0432 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0435. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043e\u0431\u0440\u0430\u0442\u0438\u043c\u0441\u044f \u043a \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0439 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 React, \u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043f\u0440\u0438\u043c\u0435\u0440 \u043f\u0440\u043e\u0441\u0442\u0435\u0439\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f todo-list.<\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">\u041f\u0440\u0438\u043c\u0435\u0440 todo-list \u0441 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430 React<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">class TodoApp extends React.Component {   constructor(props) {     super(props);     this.state = { items: [], text: '' };     this.handleChange = this.handleChange.bind(this);     this.handleSubmit = this.handleSubmit.bind(this);   }    render() {     return (       &lt;div&gt;         &lt;h3&gt;\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0435\u043b&lt;\/h3&gt;         &lt;TodoList items={this.state.items} \/&gt;         &lt;form onSubmit={this.handleSubmit}&gt;           &lt;label htmlFor=&quot;new-todo&quot;&gt;             \u0427\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c?           &lt;\/label&gt;           &lt;input             id=&quot;new-todo&quot;             onChange={this.handleChange}             value={this.state.text}           \/&gt;           &lt;button&gt;             \u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c #{this.state.items.length + 1}           &lt;\/button&gt;         &lt;\/form&gt;       &lt;\/div&gt;     );   }    handleChange(e) {     this.setState({ text: e.target.value });   }    handleSubmit(e) {     e.preventDefault();     if (!this.state.text.length) {       return;     }     const newItem = {       text: this.state.text,       id: Date.now()     };     this.setState(state =&gt; ({       items: state.items.concat(newItem),       text: ''     }));   } }  class TodoList extends React.Component {   render() {     return (       &lt;ul&gt;         {this.props.items.map(item =&gt; (           &lt;li key={item.id}&gt;{item.text}&lt;\/li&gt;         ))}       &lt;\/ul&gt;     );   } } <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  <i>\u00ab\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <b>props<\/b> \u0438 <b>state<\/b>, \u043c\u043e\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u0434\u0435\u043b. \u0412 \u044d\u0442\u043e\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <b>state<\/b> \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u2026 \u201e<\/i><\/p>\n<p>  \u0414\u043b\u044f \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0435\u0433\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u0430 (\u0442.\u0435. \u043c\u0435\u043d\u044f \u043f\u0430\u0440\u0443 \u043b\u0435\u0442 \u043d\u0430\u0437\u0430\u0434) \u044d\u0442\u0430 \u0444\u0440\u0430\u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0432\u044b\u0432\u043e\u0434: \u201c\u0412\u043e\u0442 \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 todo-list \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u00bb. \u041d\u043e \u043a\u0442\u043e \u0445\u0440\u0430\u043d\u0438\u0442 \u0441\u0442\u0435\u0439\u0442 \u0432 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435?! \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0436\u0435 \u0435\u0441\u0442\u044c state management \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438.<\/p>\n<p>  <a href=\"https:\/\/redux.js.org\/basics\/example\/\" rel=\"nofollow\">\u041f\u0440\u0438\u043c\u0435\u0440 todo-list \u0438\u0437 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 Redux<\/a><\/p>\n<p>  \u0414\u0430, \u0442\u0430\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u043b\u043e \u043a\u0443\u0434\u0430 \u043f\u043e\u043d\u044f\u0442\u043d\u0435\u0435 \u0438 \u043f\u0440\u043e\u0449\u0435 <b>(\u043d\u0435\u0442)<\/b>. \u041c\u043e\u0436\u0435\u0442 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0438?<\/p>\n<h2>\u041d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435<\/h2>\n<p>  \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 \u043e todo-list \u043d\u0435 \u043a\u0430\u043a \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0435\u0440\u044b, \u0442\u043e \u0435\u0441\u0442\u044c \u0437\u0430\u0431\u0443\u0434\u0435\u043c, \u0447\u0442\u043e \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0440\u0438\u0441\u043e\u0432\u0430\u0442\u044c HTML (\u00abweb \u2014 \u044d\u0442\u043e \u0434\u0435\u0442\u0430\u043b\u044c\u00bb). \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0433\u043b\u0430\u0437\u0430\u043c\u0438 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u043c\u044b \u043d\u0435 \u0441\u043c\u043e\u0436\u0435\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u0440\u0438\u0434\u0435\u0442\u0441\u044f \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b (\u043a\u0430\u043a \u0433\u043e\u0432\u043e\u0440\u0438\u0442 \u0434\u044f\u0434\u044e\u0448\u043a\u0430 \u0411\u043e\u0431, \u00ab\u043c\u043e\u0436\u043d\u043e \u0442\u043e\u0433\u0434\u0430 \u0438 TDD \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c\u00bb). \u0410 \u0432 \u0447\u0451\u043c \u0437\u0430\u0434\u0430\u0447\u0430? \u0427\u0442\u043e \u0437\u0430 todo-list? \u041f\u0440\u043e\u0431\u0443\u0435\u043c \u043f\u0438\u0441\u0430\u0442\u044c.<\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">Todo.spec.ts<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">import { Todo } from '.\/Todo';  describe('Todo', () =&gt; {   let todo: Todo;    beforeEach(() =&gt; {     todo = new Todo('description');   });    it('+getItems() should returns Todo[]', () =&gt; {     expect(todo.getTitle()).toBe('description');   });    it('+isCompleted() should returns completion flag', () =&gt; {     expect(todo.isCompleted()).toBe(false);   });    it('+toggleCompletion() should invert completion flag', () =&gt; {     todo.toggleCompletion();     expect(todo.isCompleted()).toBe(true);   }); }); <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  <\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">TodoList.spec.ts<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">import { TodoList } from '.\/TodoList';  describe('TodoList', () =&gt; {   let todoList: TodoList;    beforeEach(() =&gt; {     todoList = new TodoList();   });    it('+getItems() should returns Todo[]', () =&gt; {     expect(todoList.getItems()).toEqual([]);   });    it('+add() should create item and add to collection', () =&gt; {     todoList.add('Write tests');     expect(todoList.getItems()).toHaveLength(1);   });    it('+add() should create item with the description', () =&gt; {     const description = 'Write tests';     todoList.add(description);     const [item] = todoList.getItems();     expect(item.getTitle()).toBe(description);   });    it('+getCompletedItems() should not returns uncompleted Todo[]', () =&gt; {     const description = 'Write tests';     todoList.add(description);     expect(todoList.getCompletedItems()).toEqual([]);   });    it('+getCompletedItems() should returns completed Todo[]', () =&gt; {     const description = 'Write tests';     todoList.add(description);     const [item] = todoList.getItems();     item.toggleCompletion();     expect(todoList.getCompletedItems()).toEqual([item]);   });    it('+getUncompletedItems() should returns uncompleted Todo[]', () =&gt; {     const description = 'Write tests';     todoList.add(description);     const [item] = todoList.getItems();     expect(todoList.getUncompletedItems()).toEqual([item]);   });    it('+getUncompletedItems() should not returns completed Todo[]', () =&gt; {     const description = 'Write tests';     todoList.add(description);     const [item] = todoList.getItems();     item.toggleCompletion();     expect(todoList.getUncompletedItems()).toEqual([]);   }); }); <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  <\/p>\n<pre><code class=\"javascript\">export class Todo {   private completed: boolean = false;    constructor(private description: string) {}    getTitle(): string {     return this.description;   }    isCompleted(): boolean {     return this.completed;   }    toggleCompletion(): void {     this.completed = !this.completed;   } } <\/code><\/pre>\n<p>  <\/p>\n<pre><code class=\"javascript\">import { Todo } from '.\/Todo';  export class TodoList {   private items: Todo[] = [];    getItems(): Todo[] {     return this.items;   }    getCompletedItems(): Todo[] {     return this.items.filter((todo) =&gt; todo.isCompleted());   }    getUncompletedItems(): Todo[] {     return this.items.filter((todo) =&gt; !todo.isCompleted());   }    add(description: string): void {     this.items.push(new Todo(description));   } } <\/code><\/pre>\n<p>  \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0434\u0432\u0430 \u043f\u0440\u043e\u0441\u0442\u044b\u0445 \u043a\u043b\u0430\u0441\u0441\u0430 \u0441 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0432\u043d\u044b\u043c\u0438 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430\u043c\u0438. \u041d\u0435\u0443\u0436\u0435\u043b\u0438 \u0432\u0441\u0451? \u0422\u0435\u0441\u0442\u044b \u043f\u0440\u043e\u0445\u043e\u0434\u044f\u0442. \u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u0446\u0435\u043f\u0438\u043c \u0420\u0435\u0430\u043a\u0442.<\/p>\n<pre><code class=\"javascript\">import React from 'react';  import { TodoList } from '.\/core\/TodoList';  export class App extends React.Component {   todoList: TodoList = this.createTodoList();    render(): any {     return (       &lt;React.Fragment&gt;         &lt;header&gt;           &lt;h1&gt;Todo List App&lt;\/h1&gt;         &lt;\/header&gt;         &lt;main&gt;           &lt;TodoListCmp todoList={this.todoList}&gt;&lt;\/TodoListCmp&gt;           &lt;AddTodoCmp todoList={this.todoList}&gt;&lt;\/AddTodoCmp&gt;         &lt;\/main&gt;       &lt;\/React.Fragment&gt;     );   }    private createTodoList(): TodoList {     const todoList = new TodoList();     todoList.add('Initial created Todo');     return todoList;   } }  export const TodoListCmp: React.FC&lt;{ todoList: TodoList }&gt; = ({ todoList }) =&gt; {   return (     &lt;div&gt;       &lt;h2&gt;What to do?&lt;\/h2&gt;       &lt;ul&gt;         {todoList.getItems().map((todo) =&gt; (           &lt;li key={todo.getTitle()}&gt;{todo.getTitle()}&lt;\/li&gt;         ))}       &lt;\/ul&gt;     &lt;\/div&gt;   ); };  export const AddTodoCmp: React.FC&lt;{ todoList: TodoList }&gt; = ({ todoList }) =&gt; {   return &lt;button onClick={() =&gt; todoList.add(`Todo ${todoList.getItems().length}`)}&gt;Add&lt;\/button&gt;; }; <\/code><\/pre>\n<p>  \u0418 \u0443\u0431\u0435\u0434\u0438\u043c\u0441\u044f, \u0447\u0442\u043e\u2026 \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0425\u043c\u2026 \u0422\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u043d\u044f\u0442\u043d\u043e, \u043f\u043e\u0447\u0435\u043c\u0443 \u0432\u0441\u0435 \u043d\u0430\u0434\u043e \u043f\u0438\u0441\u0430\u0442\u044c \u0432 <b>state<\/b> \u2014 \u0447\u0442\u043e\u0431\u044b \u0420\u0435\u0430\u043a\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u0435\u0440\u0435\u0440\u0438\u0441\u043e\u0432\u044b\u0432\u0430\u043b\u0441\u044f, \u0443\u0437\u043d\u0430\u0432 \u043e\u0431 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0445. \u041d\u043e \u0440\u0430\u0437\u0432\u0435 \u044d\u0442\u043e \u043f\u043e\u0432\u043e\u0434 \u043d\u0430\u0440\u0443\u0448\u0438\u0442\u044c \u0432\u0441\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0435 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u044b \u0438 \u043f\u043e\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u0432\u043e view \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442? \u041d\u0435\u043c\u043d\u043e\u0436\u043a\u043e \u0442\u0435\u0440\u043f\u0435\u043d\u0438\u044f \u0438 \u043e\u0442\u0432\u0430\u0433\u0438. \u0414\u043b\u044f \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0435\u043a\u0440\u0430\u0441\u043d\u043e \u043f\u043e\u0434\u043e\u0439\u0434\u0435\u0442 <s>\u0432\u044b\u0437\u043e\u0432 forceUpdate() \u0432 \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e\u043c \u0446\u0438\u043a\u043b\u0435 \u0438\u043b\u0438<\/s> <a href=\"https:\/\/refactoring.guru\/ru\/design-patterns\/observer\" rel=\"nofollow\">\u043f\u0430\u0442\u0442\u0435\u0440\u043d \u041d\u0430\u0431\u043b\u044e\u0434\u0430\u0442\u0435\u043b\u044c<\/a>.<\/p>\n<p>  \u041c\u043d\u0435 \u043d\u0440\u0430\u0432\u0438\u0442\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 RxJs, \u043d\u043e \u044f \u043d\u0435 \u0431\u0443\u0434\u0443 \u0435\u0451 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c, \u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u044e \u0435\u0451 API \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0435 \u0434\u043b\u044f \u043d\u0430\u0448\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438.<\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">Observable.spec.ts<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">import { Observable, Subject } from '.\/Observable';  describe('Observable', () =&gt; {   let subject: Subject&lt;any&gt;;   let observable: Observable&lt;any&gt;;    beforeEach(() =&gt; {     subject = new Subject();     observable = subject.asObservable();   });    it('should call callback on next value', async () =&gt; {     const spy = jasmine.createSpy();     observable.subscribe(spy);     subject.next({});     await delay();     expect(spy).toHaveBeenCalled();   });    it('should not call callback on next value if unsubscribed', async () =&gt; {     const spy = jasmine.createSpy();     const subscription = observable.subscribe(spy);     subscription.unsubscribe();     subject.next({});     await delay();     expect(spy).not.toHaveBeenCalled();   });    it('should send to callback subject.next value', async () =&gt; {     const spy = jasmine.createSpy();     observable.subscribe(spy);     const sendingValue = {};     subject.next(sendingValue);     await delay();     expect(spy.calls.first().args[0]).toBe(sendingValue);   }); });  function delay(timeoutInMs?: number): Promise&lt;void&gt; {   return new Promise((resolve) =&gt; setTimeout(resolve, timeoutInMs)); } <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  <\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">Observable.ts<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">export interface Observable&lt;T = unknown&gt; {   subscribe(onNext: (value: T) =&gt; void): Subscription; }  export interface Subscription {   unsubscribe(): void; }  export class Subject&lt;T = unknown&gt; implements Observable&lt;T&gt; {   protected callbackSet: Set&lt;(value: T) =&gt; void&gt; = new Set();    asObservable(): Observable&lt;T&gt; {     return this;   }    subscribe(onNext: (value: T) =&gt; void): Subscription {     this.callbackSet.add(onNext);     return { unsubscribe: () =&gt; this.callbackSet.delete(onNext) };   }    next(value: T): void {     Promise.resolve().then(() =&gt; this.callbackSet.forEach((onNext) =&gt; onNext(value)));   } } <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  \u041f\u043e-\u043c\u043e\u0435\u043c\u0443, \u0442\u043e\u0436\u0435 \u043d\u0438\u0447\u0435\u0433\u043e \u0441\u043b\u043e\u0436\u043d\u043e\u0433\u043e. \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0442\u0435\u0441\u0442 (\u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435 \u043e\u0431 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0445 \u2014 \u0442\u0430 \u0435\u0449\u0451 \u043b\u043e\u0433\u0438\u043a\u0430).<\/p>\n<pre><code class=\"javascript\">  it('+TodoList.prototype.add() should emit changes', async () =&gt; {     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     todoList.add('description');     await delay();     expect(spy).toHaveBeenCalled();   });<\/code><\/pre>\n<p>  \u041d\u0430 \u043c\u0438\u043d\u0443\u0442\u043a\u0443 \u0437\u0430\u0434\u0443\u043c\u0430\u0435\u043c\u0441\u044f, \u0430 \u0432\u043b\u0438\u044f\u0442 \u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438 Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u043d\u0430 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 TodoList? \u0412\u043b\u0438\u044f\u0435\u0442 \u2014 \u043c\u0435\u0442\u043e\u0434\u044b getCompletedItems\/getUncompletedItems \u0434\u043e\u043b\u0436\u043d\u044b \u0432\u0435\u0440\u043d\u0443\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 \u043d\u0430\u0431\u043e\u0440 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u0442\u043e\u0438\u0442 \u043f\u0435\u0440\u0435\u043d\u0435\u0441\u0442\u0438 toggleCompletion \u0432 \u043a\u043b\u0430\u0441\u0441 TodoList? \u041f\u043b\u043e\u0445\u0430\u044f \u0438\u0434\u0435\u044f \u2014 \u0441 \u0442\u0430\u043a\u0438\u043c \u043f\u043e\u0434\u0445\u043e\u0434\u043e\u043c \u043d\u0430\u043c \u043f\u0440\u0438\u0434\u0451\u0442\u0441\u044f \u0440\u0430\u0437\u0434\u0443\u0432\u0430\u0442\u044c TodoList \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u0444\u0438\u0447\u0438, \u043a\u0430\u0441\u0430\u044e\u0449\u0435\u0439\u0441\u044f \u043d\u043e\u0432\u043e\u0433\u043e Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 (\u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435 \u043a \u044d\u0442\u043e\u043c\u0443 \u0432\u0435\u0440\u043d\u0435\u043c\u0441\u044f). \u041d\u043e \u043a\u0430\u043a \u0443\u0437\u043d\u0430\u0432\u0430\u0442\u044c \u043e\u0431 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0445, \u043e\u043f\u044f\u0442\u044c \u041d\u0430\u0431\u043b\u044e\u0434\u0430\u0442\u0435\u043b\u044c? \u041f\u043e\u0441\u0442\u0443\u043f\u0438\u043c \u043f\u0440\u043e\u0449\u0435, \u043f\u0443\u0441\u0442\u044c Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0441\u0430\u043c \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u0442 \u043e\u0431 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u0445 \u0447\u0435\u0440\u0435\u0437 callback.<\/p>\n<p>  \u041f\u043e\u043b\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a.<\/p>\n<pre><code class=\"javascript\">import React from 'react'; import { Observable, Subject } from 'src\/utils\/Observable'; import { generateId } from 'src\/utils\/generateId';  export class Todo {   private completed: boolean = false;    id: string = generateId();    constructor(private description: string, private onCompletionToggle?: (todo: Todo) =&gt; void) {}    getTitle(): string {     return this.description;   }    isCompleted(): boolean {     return this.completed;   }    toggleCompletion(): void {     this.completed = !this.completed;     this.onCompletionToggle?.(this);   } }  export class TodoList {   private items: Todo[] = [];   private changesSubject = new Subject();    readonly changes: Observable = this.changesSubject.asObservable();    getItems(): Todo[] {     return this.items;   }    getCompletedItems(): Todo[] {     return this.items.filter((todo) =&gt; todo.isCompleted());   }    getUncompletedItems(): Todo[] {     return this.items.filter((todo) =&gt; !todo.isCompleted());   }    add(description: string): void {     this.items.push(new Todo(description, () =&gt; this.changesSubject.next({})));     this.changesSubject.next({});   } }  export class App extends React.Component {   todoList: TodoList = this.createTodoList();    render(): any {     return (       &lt;React.Fragment&gt;         &lt;header&gt;           &lt;h1&gt;Todo List App&lt;\/h1&gt;         &lt;\/header&gt;         &lt;main&gt;           &lt;TodoListCmp todoList={this.todoList}&gt;&lt;\/TodoListCmp&gt;           &lt;AddTodoCmp todoList={this.todoList}&gt;&lt;\/AddTodoCmp&gt;         &lt;\/main&gt;       &lt;\/React.Fragment&gt;     );   }    componentDidMount(): void {     this.todoList.changes.subscribe(() =&gt; this.forceUpdate());   }    private createTodoList(): TodoList {     const todoList = new TodoList();     todoList.add('Initial created Todo');     return todoList;   } }  export const TodoListCmp: React.FC&lt;{ todoList: TodoList }&gt; = ({ todoList }) =&gt; {   return (     &lt;div&gt;       &lt;h2&gt;What to do?&lt;\/h2&gt;       &lt;ul&gt;         {todoList.getUncompletedItems().map((todo) =&gt; (           &lt;TodoCmp key={todo.id} todo={todo}&gt;&lt;\/TodoCmp&gt;         ))}         {todoList.getCompletedItems().map((todo) =&gt; (           &lt;TodoCmp key={todo.id} todo={todo}&gt;&lt;\/TodoCmp&gt;         ))}       &lt;\/ul&gt;     &lt;\/div&gt;   ); };  export const TodoCmp: React.FC&lt;{ todo: Todo }&gt; = ({ todo }) =&gt; (   &lt;li     style={{ textDecoration: todo.isCompleted() ? 'line-through' : '' }}     onClick={() =&gt; todo.toggleCompletion()}   &gt;     {todo.getTitle()}   &lt;\/li&gt; );  export const AddTodoCmp: React.FC&lt;{ todoList: TodoList }&gt; = ({ todoList }) =&gt; {   return &lt;button onClick={() =&gt; todoList.add(`Todo ${todoList.getItems().length}`)}&gt;Add&lt;\/button&gt;; }; <\/code><\/pre>\n<p>  \u041f\u043e\u0445\u043e\u0436\u0435, \u0442\u0430\u043a \u0438 \u0434\u043e\u043b\u0436\u043d\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0435 \u043e\u0442 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 todo-list. \u0415\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u2014 \u042f\u041f. \u041c\u043e\u0436\u043d\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u043a\u043e\u043d\u0441\u043e\u043b\u0438, \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Angular.<\/p>\n<p>  \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u043b\u043e\u0436\u043d\u044b\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0433\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0430 \u0438 \u043f\u0440\u043e\u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u0441\u0438\u043b\u044c\u043d\u044b\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u043c \u0432\u043e\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u0441\u043c\u043e\u0434\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u043f\u0440\u0430\u0432\u0434\u043e\u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u0440\u0430\u0437\u0432\u0438\u0442\u0438\u044f todo-list.<\/p>\n<h2>\u041f\u0440\u0430\u0432\u043a\u0438 \u043e\u0442 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u0430<\/h2>\n<p>  \u0413\u043b\u0430\u0432\u043d\u044b\u0439 \u043a\u043e\u0448\u043c\u0430\u0440 \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 \u2014 \u0438\u0437\u043c\u0435\u043d\u044f\u044e\u0449\u0438\u0435\u0441\u044f \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f. \u0412\u044b \u0437\u043d\u0430\u0435\u0442\u0435, \u0447\u0442\u043e \u043f\u0440\u0430\u0432\u043e\u043a \u043d\u0435 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c, \u0438 \u0437\u043d\u0430\u0435\u0442\u0435, \u0447\u0442\u043e \u044d\u0442\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e. \u041d\u043e \u043a\u0430\u043a \u0432\u044b \u0433\u043e\u0442\u043e\u0432\u0438\u0442\u0435\u0441\u044c \u043a \u0433\u0440\u044f\u0434\u0443\u0449\u0438\u043c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u043c?<\/p>\n<h3>\u041e\u0441\u043e\u0431\u044b\u0435 Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b<\/h3>\n<p>  \u041e\u0434\u043d\u043e\u0439 \u0438\u0437 \u043a\u043b\u044e\u0447\u0435\u0432\u044b\u0445 \u0444\u0438\u0448\u0435\u043a \u041e\u041e\u041f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0435\u0448\u0430\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0443 \u0447\u0435\u0440\u0435\u0437 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u044b\u0445 \u0442\u0438\u043f\u043e\u0432. \u041f\u043e\u0436\u0430\u043b\u0443\u0439, \u044d\u0442\u0430 \u0441\u0430\u043c\u044b\u0439 \u0441\u0438\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0438\u0451\u043c \u041e\u041e\u041f, \u0441\u043f\u043e\u0441\u043e\u0431\u043d\u044b\u0439 \u0432 \u043e\u0434\u0438\u043d\u043e\u0447\u043a\u0443 \u0432\u044b\u0442\u0430\u0449\u0438\u0442\u044c \u0441\u043b\u043e\u0436\u043d\u0443\u044e \u0438 \u0433\u0440\u043e\u043c\u043e\u0437\u0434\u043a\u0443\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044f \u043d\u0435 \u0437\u043d\u0430\u044e, \u0447\u0442\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0442 Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0443\u043c\u0435\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c \u0435\u0433\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435, \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0434\u0451\u0442\u0441\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044b, \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0435\u0448\u0430\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u044f\u043c\u043e\u0435 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 SpaceX\u2026 \u041d\u043e \u044f \u0443\u0432\u0435\u0440\u0435\u043d, \u0447\u0442\u043e \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u0441\u044f, \u0438 \u043c\u043d\u0435 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u044f\u0442\u0441\u044f \u0440\u0430\u0437\u043d\u044b\u0435 \u0442\u0438\u043f\u044b Todo.<\/p>\n<pre><code class=\"javascript\">export class EditableTodo extends Todo {   changeTitle(title: string): void {     this.title = title;     this.onChange?.(this);   } } <\/code><\/pre>\n<p>  \u041f\u043e\u0445\u043e\u0436\u0435, \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430, \u043d\u0430\u043c \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043c\u0435\u043d\u044f\u0442\u044c \u0438 view-\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b. \u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u044f \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u043b (\u0438 \u043f\u0438\u0441\u0430\u043b) \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b, \u0432 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043c\u0438\u043b\u043b\u0438\u043e\u043d \u0440\u0430\u0437\u043d\u044b\u0445 \u0443\u0441\u043b\u043e\u0432\u0438\u0439 \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442 div-\u0431\u043b\u043e\u043a \u0438\u0437 \u0436\u0438\u0440\u0430\u0444\u0430 \u0432 \u043f\u0443\u043b\u0435\u043c\u0451\u0442. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043c\u043e\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c hoc-\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441 \u043e\u0433\u0440\u043e\u043c\u043d\u044b\u043c switch-case \u0441\u043f\u0438\u0441\u043a\u043e\u043c. \u0418\u043b\u0438 \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c <a href=\"https:\/\/refactoring.guru\/ru\/design-patterns\/visitor\" rel=\"nofollow\">\u043f\u0430\u0442\u0442\u0435\u0440\u043d \u041f\u043e\u0441\u0435\u0442\u0438\u0442\u0435\u043b\u044c<\/a> \u0438 \u0434\u0432\u043e\u0439\u043d\u0443\u044e \u0434\u0438\u0441\u043f\u0435\u0442\u0447\u0435\u0440\u0438\u0437\u0430\u0446\u0438\u044e, \u0438 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u044c Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443 \u0441\u0430\u043c\u043e\u043c\u0443 \u0440\u0435\u0448\u0430\u0442\u044c, \u043a\u0430\u043a\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043d\u0443\u0436\u043d\u043e \u0440\u0438\u0441\u043e\u0432\u0430\u0442\u044c.<\/p>\n<pre><code class=\"javascript\">export class Todo {   id: string = '';    constructor(     protected title: string,     private completed: boolean = false,     protected onChange?: (todo: Todo) =&gt; void,   ) {}    getTitle(): string {     return this.title;   }    isCompleted(): boolean {     return this.completed;   }    toggleCompletion(): void {     this.completed = !this.completed;     this.onChange?.(this);   }    render(renderer: TodoRenderer): any {     return renderer.renderSimpleTodo(this);   } }  export class EditableTodo extends Todo {   changeTitle(title: string): void {     this.title = title;     this.onChange?.(this);   }    render(renderer: TodoRenderer): any {     return renderer.renderEditableTodo(this);   } }  export class TodoRenderer {   renderSimpleTodo(todo: Todo): any {     return &lt;SimpleTodoCmp todo={todo}&gt;&lt;\/SimpleTodoCmp&gt;;   }    renderFixedTodo(todo: Todo): any {     return &lt;FixedTodoCmp todo={todo}&gt;&lt;\/FixedTodoCmp&gt;;   }    renderEditableTodo(todo: EditableTodo): any {     return &lt;EditableTodoCmp todo={todo}&gt;&lt;\/EditableTodoCmp&gt;;   } } <\/code><\/pre>\n<p>  \u0412\u0430\u0440\u0438\u0430\u043d\u0442 \u0441 \u0434\u0432\u043e\u0439\u043d\u043e\u0439 \u0434\u0438\u0441\u043f\u0435\u0442\u0447\u0438\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e \u043f\u043e\u043b\u0435\u0437\u0435\u043d, \u043a\u043e\u0433\u0434\u0430 \u0443 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0435\u0441\u0442\u044c \u0440\u0430\u0437\u043d\u044b\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f. \u041c\u043e\u0436\u043d\u043e \u043c\u0435\u043d\u044f\u0442\u044c \u0438\u0445, \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u044f \u0432 \u043c\u0435\u0442\u043e\u0434 render \u0440\u0430\u0437\u043d\u044b\u0435 TodoRenderer-\u044b.<\/p>\n<p>  \u0422\u0435\u043f\u0435\u0440\u044c \u043c\u044b \u0433\u043e\u0442\u043e\u0432\u044b. \u0421\u0442\u0440\u0430\u0445 \u043f\u0435\u0440\u0435\u0434 \u043d\u043e\u0432\u044b\u043c\u0438 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u043e\u0431 \u00ab\u043e\u0441\u043e\u0431\u044b\u0445\u00bb Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0445 \u0438\u0441\u0447\u0435\u0437. \u0414\u0443\u043c\u0430\u044e, \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u0441\u0430\u043c\u0438 \u043c\u043e\u0433\u043b\u0438 \u0431\u044b \u043f\u0440\u043e\u044f\u0432\u0438\u0442\u044c \u0438\u043d\u0438\u0446\u0438\u0430\u0442\u0438\u0432\u0443, \u0438 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0438\u0442\u044c \u043f\u0430\u0440\u0443 \u0444\u0438\u0447, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0445 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u0442\u0438\u043f\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u0439\u0447\u0430\u0441 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0437\u0430 \u0441\u0447\u0451\u0442 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0438 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0433\u043e.<\/p>\n<h3>\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440<\/h3>\n<p>  \u0427\u0442\u043e \u0437\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c? \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u043d\u0443\u0436\u043d\u043e \u0443\u043c\u0435\u0442\u044c \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u043d\u0430\u0448 \u0441\u043f\u0438\u0441\u043e\u043a \u0447\u0435\u0440\u0435\u0437 HTTP \u2014 \u0435\u0449\u0451 \u043e\u0434\u043d\u043e \u043d\u043e\u0432\u043e\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0435. \u041f\u0440\u043e\u0431\u0443\u0435\u043c \u0440\u0435\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.<\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">AppTodoList.spec.ts<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">import { delay } from 'src\/utils\/delay'; import { TodoType } from '..\/core\/TodoFactory'; import { AppTodoList } from '.\/AppTodoList';  describe('AppTodoList', () =&gt; {   let todoList: AppTodoList;    beforeEach(() =&gt; {     todoList = new AppTodoList({       getItems: async () =&gt; [{ type: TodoType.Simple, title: 'Loaded todo', completed: false }],       save: () =&gt; delay(),     });   });    it('+resolve() should load saved todo items', async () =&gt; {     await todoList.resolve();     expect(todoList.getItems().length).toBeGreaterThan(0);   });    it('+resolve() should emit changes', async () =&gt; {     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     await todoList.resolve();     await delay(1);     expect(spy).toHaveBeenCalled();   });    it('+add() should emit changes', async () =&gt; {     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     todoList.add({ title: '' });     await delay(1);     expect(spy).toHaveBeenCalled();   });    it('+add() should emit changes after resolve()', async () =&gt; {     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     await todoList.resolve();     todoList.add({ title: '' });     await delay(1);     expect(spy).toHaveBeenCalledTimes(2);   });    it('+todo.onChange() should emit changes', async () =&gt; {     await todoList.resolve();     await delay(1);     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     const [todo] = todoList.getItems();     todo.toggleCompletion();     await delay(1);     expect(spy).toHaveBeenCalled();   });    it('+add() should call TodoListApi.save', async () =&gt; {     const spy = jasmine.createSpy();     todoList = new AppTodoList({ getItems: async () =&gt; [], save: async () =&gt; spy() });     todoList.add({ title: '' });     await delay();     expect(spy).toHaveBeenCalled();   });    it('+todo.onChange() should call TodoListApi.save', async () =&gt; {     const spy = jasmine.createSpy();     todoList = new AppTodoList({ getItems: async () =&gt; [{ title: '' }], save: async () =&gt; spy() });     await todoList.resolve();     const [todo] = todoList.getItems();     todo.toggleCompletion();     await delay();     expect(spy).toHaveBeenCalled();   });    it('+add() should save todoList state on success and rollback to it on error', async () =&gt; {     const api = { getItems: async () =&gt; [], save: () =&gt; Promise.resolve() };     todoList = new AppTodoList(api);     todoList.add({ title: '1' });     await delay(1);     const savedData = JSON.stringify(todoList.getItems());     api.save = () =&gt; Promise.reject('Mock saving failed');     todoList.add({ title: '2' });     expect(todoList.getItems()).toHaveLength(2);     await delay(1);     expect(JSON.stringify(todoList.getItems())).toBe(savedData);   }); }); <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  <\/p>\n<pre><code class=\"javascript\">export interface TodoListApi {   getItems(): Promise&lt;TodoParams[]&gt;;   save(todoParamsList: TodoParams[]): Promise&lt;void&gt;; }  export class AppTodoList implements TodoList {   private todoFactory = new TodoFactory();   private changesSubject = new Subject();    changes: Observable = this.changesSubject.asObservable();    private state: TodoList = new TodoListImp();   private subscription: Subscription = this.state.changes.subscribe(() =&gt; this.onStateChanges());   private synchronizedTodoParamsList: TodoParams[] = [];    constructor(private api: TodoListApi) {}    async resolve(): Promise&lt;void&gt; {     const todoParamsList = await this.api.getItems();     this.updateState(todoParamsList);   }    private updateState(todoParamsList: TodoParams[]): void {     const todoList = new TodoListImp(todoParamsList);     this.state = todoList;     this.subscription.unsubscribe();     this.subscription = todoList.changes.subscribe(() =&gt; this.onStateChanges());     this.synchronizedTodoParamsList = todoParamsList;     this.changesSubject.next({});   }    private async onStateChanges(): Promise&lt;void&gt; {     this.changesSubject.next({});     try {       const params = this.state.getItems().map((todo) =&gt; this.todoFactory.serializeTodo(todo));       await this.api.save(params);       this.synchronizedTodoParamsList = params;     } catch {       this.updateState(this.synchronizedTodoParamsList);     }   }    destroy(): void {     this.subscription.unsubscribe();   }    getItems(): Todo[] {     return this.state.getItems();   }    getCompletedItems(): Todo[] {     return this.state.getCompletedItems();   }    getUncompletedItems(): Todo[] {     return this.state.getUncompletedItems();   }    add(todoParams: TodoParams): void {     this.state.add(todoParams);   } } <\/code><\/pre>\n<p>  \u041c\u044b \u043d\u0435 \u0437\u043d\u0430\u0435\u043c, \u043a\u0430\u043a \u0434\u043e\u043b\u0436\u043d\u043e \u0432\u0435\u0441\u0442\u0438 \u0441\u0435\u0431\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0416\u0434\u0430\u0442\u044c, \u0447\u0442\u043e \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0448\u043b\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f? \u041f\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u043c \u043e\u0442\u043e\u0431\u0440\u0430\u0437\u0438\u0442\u044c\u0441\u044f, \u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043e\u0448\u0438\u0431\u043a\u0438 \u043e\u0442\u043a\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435? \u0418\u043b\u0438 \u0432\u043e\u0432\u0441\u0435 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0448\u0438\u0431\u043a\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f? \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a \u0442\u043e\u0436\u0435 \u044d\u0442\u043e\u0433\u043e \u043d\u0435 \u0437\u043d\u0430\u0435\u0442. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0439 \u043d\u0435\u0438\u0437\u0431\u0435\u0436\u043d\u044b, \u043d\u043e \u043e\u043d\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u0432\u043b\u0438\u044f\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430 \u043e\u0434\u0438\u043d \u043a\u043b\u0430\u0441\u0441, \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0437\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435. \u0410 \u043d\u0430 \u043f\u043e\u0434\u0445\u043e\u0434\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u0440\u0430\u0432\u043a\u0430.<\/p>\n<h3>\u041f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435 \u043f\u043e \u0438\u0441\u0442\u043e\u0440\u0438\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439<\/h3>\n<p>  \u00ab\u041d\u0443\u0436\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0442\u043c\u0435\u043d\u044b\/\u043f\u043e\u0432\u0442\u043e\u0440\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439\u00bb\u2026<\/p>\n<p>  \u041a\u0430\u0436\u0435\u0442\u0441\u044f, \u0432\u043e\u043b\u043d\u0430 \u043d\u043e\u0432\u044b\u0445 \u043f\u0440\u0430\u0432\u043e\u043a \u0437\u0430\u0441\u0442\u0438\u0433\u0430\u0435\u0442 \u043d\u0430\u0441 \u0432\u0440\u0430\u0441\u043f\u043b\u043e\u0445. \u041d\u043e \u043d\u0438 \u0432 \u043a\u043e\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u043d\u0435\u043b\u044c\u0437\u044f \u0436\u0435\u0440\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u0430\u043c\u0438. \u0421\u0435\u0439\u0447\u0430\u0441 \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u043e \u043d\u0435 \u043f\u043e\u043d\u044f\u0442\u043d\u043e \u043a\u0430\u043a\u0430\u044f \u0438\u0435\u0440\u0430\u0440\u0445\u0438\u044f \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u043b\u0443\u0447\u0448\u0435, \u0438 \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u043b\u0438 \u043d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u043e\u043e\u0431\u0449\u0435. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0438\u0447\u0435\u0433\u043e \u0441\u0442\u0440\u0430\u0448\u043d\u043e\u0433\u043e \u043d\u0435 \u0441\u043b\u0443\u0447\u0438\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u043c \u043d\u0430\u0448 \u0433\u0440\u044f\u0437\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 (\u0431\u0443\u0434\u0435\u043c \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u044d\u0442\u043e \u0434\u0435\u0442\u0430\u043b\u044f\u043c\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438), \u043f\u043e\u0436\u0435\u0440\u0442\u0432\u043e\u0432\u0430\u0432 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u043e\u043c \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438.<\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">TodoListHistory.spec.ts<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">import { TodoParams } from 'src\/core\/TodoFactory'; import { delay } from 'src\/utils\/delay'; import { TodoListHistory } from '.\/TodoListHistory';  describe('TodoListHistory', () =&gt; {   let history: TodoListHistory;    beforeEach(() =&gt; {     history = new TodoListHistory();   });    it('+getState() should returns TodoParams[]', () =&gt; {     expect(history.getState()).toEqual([]);   });    it('+setState() should rewrite current state', () =&gt; {     const newState = [{ title: '' }] as TodoParams[];     history.setState(newState);     expect(history.getState()).toBe(newState);   });    it('+hasPrev() should returns false on init', () =&gt; {     expect(history.hasPrev()).toBe(false);   });    it('+hasPrev() should returns true after setState()', () =&gt; {     history.setState([]);     expect(history.hasPrev()).toBe(true);   });    it('+switchToPrev() should switch on prev state', () =&gt; {     const prevState = [{ title: '' }] as TodoParams[];     history.setState(prevState);     history.setState([]);     history.switchToPrev();     expect(history.getState()).toBe(prevState);   });    it('+hasPrev() should returns false after switch to first', () =&gt; {     history.setState([]);     history.switchToPrev();     expect(history.hasPrev()).toBe(false);   });    it('+hasNext() should returns false on init', () =&gt; {     expect(history.hasNext()).toBe(false);   });    it('+hasNext() should returns true after switchToPrev()', () =&gt; {     history.setState([]);     history.switchToPrev();     expect(history.hasNext()).toBe(true);   });    it('+switchToNext() should switch on next state', () =&gt; {     const prevState = [{ title: '' }] as TodoParams[];     history.setState([]);     history.setState(prevState);     history.switchToPrev();     history.switchToNext();     expect(history.getState()).toBe(prevState);   });    it('+hasNext() should returns false after switchToNext()', () =&gt; {     history.setState([]);     history.switchToPrev();     history.switchToNext();     expect(history.hasNext()).toBe(false);   });    it('+hasNext() should returns false after setState()', () =&gt; {     history.setState([]);     history.switchToPrev();     history.setState([]);     expect(history.hasNext()).toBe(false);   });    it('+switchToPrev() should switch on prev state after setState()', () =&gt; {     const prevState = [{ title: '' }] as TodoParams[];     history.setState(prevState);     history.setState([]);     history.switchToPrev();     history.setState([]);     history.switchToPrev();     expect(history.getState()).toBe(prevState);   });    it('+setState() should not emit changes', async () =&gt; {     const spy = jasmine.createSpy();     history.changes.subscribe(spy);     history.setState([]);     await delay();     expect(spy).not.toHaveBeenCalled();   });    it('+switchToPrev() should emit changes', async () =&gt; {     history.setState([]);     await delay();     const spy = jasmine.createSpy();     history.changes.subscribe(spy);     history.switchToPrev();     await delay();     expect(spy).toHaveBeenCalled();   });    it('+switchToPrev() should emit changes', async () =&gt; {     history.setState([]);     history.switchToPrev();     await delay();     const spy = jasmine.createSpy();     history.changes.subscribe(spy);     history.switchToNext();     await delay();     expect(spy).toHaveBeenCalled();   });    it('+reset() should reset history and apply initial state', async () =&gt; {     history.setState([]);     expect(history.hasPrev()).toBe(true);     const initState = [{ title: '' }] as TodoParams[];     history.reset(initState);     expect(history.hasPrev()).toBe(false);     expect(history.getState()).toBe(initState);   }); }); <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  <\/p>\n<div class=\"spoiler\"><b class=\"spoiler_title\">AppTodoList.spec.ts<\/b><\/p>\n<div class=\"spoiler_text\">\n<pre><code class=\"javascript\">import { delay } from 'src\/utils\/delay'; import { TodoType } from '..\/core\/TodoFactory'; import { AppTodoList } from '.\/AppTodoList';  describe('AppTodoList', () =&gt; {   let todoList: AppTodoList;    beforeEach(() =&gt; {     todoList = new AppTodoList({       getItems: async () =&gt; [{ type: TodoType.Simple, title: 'Loaded todo', completed: false }],       save: () =&gt; delay(),     });   });    it('+resolve() should load saved todo items', async () =&gt; {     await todoList.resolve();     expect(todoList.getItems().length).toBeGreaterThan(0);   });    it('+resolve() should emit changes', async () =&gt; {     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     await todoList.resolve();     await delay();     expect(spy).toHaveBeenCalled();   });    it('+add() should emit changes', async () =&gt; {     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     todoList.add({ title: '' });     await delay();     expect(spy).toHaveBeenCalled();   });    it('+add() should emit changes after resolve()', async () =&gt; {     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     await todoList.resolve();     todoList.add({ title: '' });     await delay();     expect(spy).toHaveBeenCalledTimes(2);   });    it('+todo.onChange() should emit changes', async () =&gt; {     await todoList.resolve();     await delay();     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     const [todo] = todoList.getItems();     todo.toggleCompletion();     await delay();     expect(spy).toHaveBeenCalled();   });    it('+add() should call TodoListApi.save', async () =&gt; {     const spy = jasmine.createSpy();     todoList = new AppTodoList({ getItems: async () =&gt; [], save: async () =&gt; spy() });     todoList.add({ title: '' });     await delay();     expect(spy).toHaveBeenCalled();   });    it('+todo.onChange() should call TodoListApi.save', async () =&gt; {     const spy = jasmine.createSpy();     todoList = new AppTodoList({ getItems: async () =&gt; [{ title: '' }], save: async () =&gt; spy() });     await todoList.resolve();     const [todo] = todoList.getItems();     todo.toggleCompletion();     await delay();     expect(spy).toHaveBeenCalled();   });    it('+add() should ignore error on save', async () =&gt; {     const api = { getItems: async () =&gt; [], save: () =&gt; Promise.resolve() };     todoList = new AppTodoList(api);     todoList.add({ title: '1' });     await delay();     api.save = jasmine.createSpy().and.returnValue(Promise.reject('Mock saving failed'));     todoList.add({ title: '2' });     expect(todoList.getItems()).toHaveLength(2);     await delay();     expect(api.save).toHaveBeenCalled();     expect(todoList.getItems()).toHaveLength(2);   });    it('+resolve() should provide current todoList state to history', async () =&gt; {     expect(todoList.getItems()).toHaveLength(0);     expect(todoList.getHistory().getState()).toHaveLength(0);     await todoList.resolve();     await delay();     expect(todoList.getItems()).toHaveLength(1);     expect(todoList.getHistory().getState()).toHaveLength(1);   });    it('+add() should provide current todoList state to history', async () =&gt; {     expect(todoList.getItems()).toHaveLength(0);     expect(todoList.getHistory().getState()).toHaveLength(0);     todoList.add({ title: '' });     await delay();     expect(todoList.getItems()).toHaveLength(1);     expect(todoList.getHistory().getState()).toHaveLength(1);   });    it('+history.switchToPrev() should change todoList state on prev', async () =&gt; {     todoList.add({ title: '' });     await delay();     expect(todoList.getItems()).toHaveLength(1);     expect(todoList.getHistory().getState()).toHaveLength(1);     todoList.getHistory().switchToPrev();     await delay();     expect(todoList.getHistory().getState()).toHaveLength(0);     expect(todoList.getItems()).toHaveLength(0);   });    it('+history.switchToPrev() should change todoList state on prev after resolve()', async () =&gt; {     await todoList.resolve();     todoList.add({ title: '' });     await delay();     expect(todoList.getItems()).toHaveLength(2);     expect(todoList.getHistory().getState()).toHaveLength(2);     todoList.getHistory().switchToPrev();     await delay();     expect(todoList.getHistory().getState()).toHaveLength(1);     expect(todoList.getItems()).toHaveLength(1);   });    it('+add() should emit changes after history.switchToPrev()', async () =&gt; {     todoList.add({ title: '' });     todoList.getHistory().switchToPrev();     await delay();     const spy = jasmine.createSpy();     todoList.changes.subscribe(spy);     todoList.add({ title: '' });     await delay();     expect(spy).toHaveBeenCalled();   }); }); <\/code><\/pre>\n<p>  <\/div>\n<\/div>\n<p>  <\/p>\n<pre><code class=\"javascript\">import { TodoParams } from 'src\/core\/TodoFactory'; import { Observable, Subject } from 'src\/utils\/Observable';  export class TodoListHistory {   private changesSubject = new Subject();   private history: TodoParams[][] = [this.state];    changes: Observable = this.changesSubject.asObservable();    constructor(private state: TodoParams[] = []) {}    reset(state: TodoParams[]): void {     this.state = state;     this.history = [this.state];   }    getState(): TodoParams[] {     return this.state;   }    setState(state: TodoParams[]): void {     this.deleteHistoryAfterCurrentState();     this.state = state;     this.history.push(state);   }    private nextState(state: TodoParams[]): void {     this.state = state;     this.changesSubject.next({});   }    private deleteHistoryAfterCurrentState(): void {     this.history = this.history.slice(0, this.getCurrentStateIndex() + 1);   }    hasPrev(): boolean {     return this.getCurrentStateIndex() &gt; 0;   }    hasNext(): boolean {     return this.getCurrentStateIndex() &lt; this.history.length - 1;   }    switchToPrev(): void {     const prevStateIndex = Math.max(this.getCurrentStateIndex() - 1, 0);     this.nextState(this.history[prevStateIndex]);   }    switchToNext(): void {     const nextStateIndex = Math.min(this.getCurrentStateIndex() + 1, this.history.length - 1);     this.nextState(this.history[nextStateIndex]);   }    private getCurrentStateIndex(): number {     return this.history.indexOf(this.state);   } }  import { Observable, Subject, Subscription } from 'src\/utils\/Observable'; import { Todo } from '..\/core\/Todo'; import { TodoFactory, TodoParams } from '..\/core\/TodoFactory'; import { TodoList, TodoListImp } from '..\/core\/TodoList'; import { TodoListApi } from '.\/TodoListApi'; import { HistoryControl, TodoListHistory } from '.\/TodoListHistory';  export class AppTodoList implements TodoList {   private readonly todoFactory = new TodoFactory();   private readonly history: TodoListHistory = new TodoListHistory();    private changesSubject = new Subject();   readonly changes: Observable = this.changesSubject.asObservable();    private state: TodoList = new TodoListImp();   private stateSubscription: Subscription = this.state.changes.subscribe(() =&gt;     this.onStateChanges(),   );   private historySubscription = this.history.changes.subscribe(() =&gt; this.onHistoryChanges());    constructor(private api: TodoListApi) {}    private onStateChanges(): void {     const params = this.state.getItems().map((todo) =&gt; this.todoFactory.serializeTodo(todo));     this.history.setState(params);     this.api.save(params).catch(() =&gt; {});     this.changesSubject.next({});   }    private onHistoryChanges(): void {     const params = this.history.getState();     this.updateStateTodoList(params);     this.api.save(params).catch(() =&gt; {});   }    private updateStateTodoList(todoParamsList: TodoParams[]): void {     const todoList = new TodoListImp(todoParamsList);     this.state = todoList;     this.stateSubscription.unsubscribe();     this.stateSubscription = this.state.changes.subscribe(() =&gt; this.onStateChanges());     this.changesSubject.next({});   }    async resolve(): Promise&lt;void&gt; {     const todoParamsList = await this.api.getItems();     this.history.reset(todoParamsList);     this.updateStateTodoList(todoParamsList);   }    destroy(): void {     this.stateSubscription.unsubscribe();     this.historySubscription.unsubscribe();   }    getHistory(): HistoryControl&lt;TodoParams[]&gt; {     return this.history;   }    getItems(): Todo[] {     return this.state.getItems();   }    getCompletedItems(): Todo[] {     return this.state.getCompletedItems();   }    getUncompletedItems(): Todo[] {     return this.state.getUncompletedItems();   }    add(todoParams: TodoParams): void {     this.state.add(todoParams);   } } <\/code><\/pre>\n<p>  \u0422\u0430\u043a \u043a\u0430\u043a \u0441 \u0438\u0441\u0442\u043e\u0440\u0438\u0435\u0439 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0432\u0441\u0451 \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u043e, \u0432\u044b\u0434\u0435\u043b\u0438\u043c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0440\u0438\u0435\u0439 todo-list \u0432 \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441.<\/p>\n<pre><code class=\"javascript\">import { Todo } from 'src\/core\/Todo'; import { Observable, Subject, Subscription } from 'src\/utils\/Observable'; import { TodoFactory, TodoParams } from '..\/core\/TodoFactory'; import { TodoList, TodoListImp } from '..\/core\/TodoList'; import { HistoryControl, HistoryState } from '.\/HistoryState';  export class HistoricalTodoList implements TodoList, HistoryControl {   protected readonly todoFactory = new TodoFactory();   protected readonly history = new HistoryState&lt;TodoParams[]&gt;([]);    private changesSubject: Subject = new Subject();   readonly changes: Observable = this.changesSubject.asObservable();    private state: TodoList = new TodoListImp();   private stateSubscription: Subscription = this.state.changes.subscribe(() =&gt;     this.onStateChanged(this.getSerializedState()),   );    constructor() {}    protected onStateChanged(params: TodoParams[]): void {     this.history.addState(params);     this.changesSubject.next({});   }    protected onHistorySwitched(): void {     this.updateState(this.history.getState());   }    protected updateState(todoParamsList: TodoParams[]): void {     this.state = new TodoListImp(todoParamsList);     this.updateStateSubscription();     this.changesSubject.next({});   }    private updateStateSubscription(): void {     this.stateSubscription.unsubscribe();     this.stateSubscription = this.state.changes.subscribe(() =&gt;       this.onStateChanged(this.getSerializedState()),     );   }    private getSerializedState(): TodoParams[] {     return this.state.getItems().map((todo) =&gt; this.todoFactory.serializeTodo(todo));   }    destroy(): void {     this.stateSubscription.unsubscribe();   }    getItems(): Todo[] {     return this.state.getItems();   }    getCompletedItems(): Todo[] {     return this.state.getCompletedItems();   }    getUncompletedItems(): Todo[] {     return this.state.getUncompletedItems();   }    add(todoParams: TodoParams): void {     this.state.add(todoParams);   }    canUndo(): boolean {     return this.history.hasPrev();   }    canRedo(): boolean {     return this.history.hasNext();   }    undo(): void {     this.history.switchToPrev();     this.onHistorySwitched();   }    redo(): void {     this.history.switchToNext();     this.onHistorySwitched();   } }  import { TodoParams } from 'src\/core\/TodoFactory'; import { HistoricalTodoList } from '.\/HistoricalTodoList'; import { TodoListApi } from '.\/TodoListApi';  export class ResolvableTodoList extends HistoricalTodoList {   constructor(private api: TodoListApi) {     super();   }    async resolve(): Promise&lt;void&gt; {     const todoParamsList = await this.api.getItems();     this.history.reset(todoParamsList);     this.updateState(todoParamsList);   }    protected onStateChanged(params: TodoParams[]): void {     super.onStateChanged(params);     this.api.save(params).catch(() =&gt; this.undo());   }    protected onHistorySwitched(): void {     super.onHistorySwitched();     this.api.save(this.history.getState()).catch(() =&gt; {});   } } <\/code><\/pre>\n<p>  \u041a\u0430\u0436\u0435\u0442\u0441\u044f, \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u043c \u043f\u0440\u0438\u0448\u043b\u043e \u0441\u0430\u043c\u043e-\u0441\u043e\u0431\u043e\u0439. \u0422\u0435\u043f\u0435\u0440\u044c \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043d\u0435 \u0437\u0430\u0431\u043e\u0442\u0438\u0442\u044c\u0441\u044f \u043e \u0442\u043e\u043c, \u043a\u0430\u043a\u0443\u044e \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044e \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0432\u044b\u0431\u0435\u0440\u0435\u0442 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a \u0432 \u043a\u043e\u043d\u0435\u0447\u043d\u043e\u043c \u0441\u0447\u0451\u0442\u0435. \u041c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0435\u043c\u0443 \u0432\u0441\u0435 3 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043d\u0430 \u0432\u044b\u0431\u043e\u0440, \u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0432 \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u043a\u043b\u0430\u0441\u0441.<\/p>\n<h2>\u0418\u0442\u043e\u0433\u0438<\/h2>\n<p>  \u041a\u0430\u0436\u0435\u0442\u0441\u044f, \u0438\u0437 \u043d\u0430\u0448\u0435\u0433\u043e todo-list \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0441\u044f \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u043f\u0440\u043e\u0442\u043e\u0442\u0438\u043f Google Keep. <a href=\"https:\/\/github.com\/justerest\/todo-list\/\" rel=\"nofollow\">\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439<\/a>, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u043f\u0440\u043e\u0431\u0435\u0436\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u0438\u0441\u0442\u043e\u0440\u0438\u0438 \u043a\u043e\u043c\u043c\u0438\u0442\u043e\u0432.<\/p>\n<p>  \u0427\u0435\u043c \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0438\u0430\u043b\u044c\u043d\u043e \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 \u043e\u0442 \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u0430 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439? \u041c\u044b \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u0435\u043b\u0438 \u043e\u0442 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a, \u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c\u0441\u044f \u0432 \u044d\u0442\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u043c\u043e\u0436\u0435\u0442 \u0447\u0435\u043b\u043e\u0432\u0435\u043a, \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0432\u0448\u0438\u0439 \u0441 \u0420\u0435\u0430\u043a\u0442. \u041d\u0430\u0448\u0438 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u0431\u044b\u043b\u0438 \u043d\u0430\u0446\u0435\u043b\u0435\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430, \u0431\u0435\u0437 \u043e\u0442\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0439 \u043d\u0430 \u0434\u0435\u0442\u0430\u043b\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043a\u043e\u0434 \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u043e\u0442\u0440\u0430\u0436\u0430\u0435\u0442 \u0440\u0435\u0448\u0430\u0435\u043c\u0443\u044e \u0437\u0430\u0434\u0430\u0447\u0443. \u041d\u0430\u043c \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442\u044c \u043b\u0435\u0433\u043a\u043e\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u044b\u0445 \u0442\u0438\u043f\u043e\u0432 Todo-\u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432, \u0438 \u043c\u044b \u0433\u043e\u0442\u043e\u0432\u044b \u043a \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044e \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f.<\/p>\n<p>  \u0421 \u043a\u0430\u043a\u0438\u043c\u0438 \u0442\u0440\u0443\u0434\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u043c\u044b \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0438\u0441\u044c? \u041c\u044b \u0440\u0435\u0448\u0438\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0430 \u041d\u0430\u0431\u043b\u044e\u0434\u0430\u0442\u0435\u043b\u044c \u0431\u0435\u0437 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043a \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0443. \u041a\u0430\u043a \u0432\u044b\u044f\u0441\u043d\u0438\u043b\u043e\u0441\u044c, \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0430 \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043b\u043e\u0441\u044c \u0434\u043b\u044f \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 (\u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u0431\u044b \u043d\u0430\u043c \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0438\u0441\u043e\u0432\u0430\u0442\u044c HTML). \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u043d\u0435 \u043f\u043e\u043d\u0435\u0441\u043b\u0438 \u0438\u0437\u0434\u0435\u0440\u0436\u0435\u043a, \u043e\u0442\u043a\u0430\u0437\u0430\u0432\u0448\u0438\u0441\u044c \u043e\u0442 \u00ab\u0443\u0441\u043b\u0443\u0433\u00bb \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0439 \u0432 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439.<\/p>\n<p>  \u0425\u043e\u0447\u0435\u0442\u0441\u044f \u043e\u0441\u043e\u0431\u043e \u043f\u043e\u0434\u0447\u0435\u0440\u043a\u043d\u0443\u0442\u044c, \u0447\u0442\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432 \u043d\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u043b\u043e \u0441\u043e\u0431\u043e\u0439 \u043d\u0438\u043a\u0430\u043a\u043e\u0439 \u0441\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u0438. \u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u0442\u044b\u0435 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0441 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0432\u043d\u044b\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043e\u043c \u043e\u0434\u043d\u043e \u0443\u0434\u043e\u0432\u043e\u043b\u044c\u0441\u0442\u0432\u0438\u0435. \u0421\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043a\u043e\u0434\u0430 \u0437\u0430\u0432\u0438\u0441\u0435\u043b\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442 \u0441\u0430\u043c\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438 \u043c\u043e\u0438\u0445 \u043d\u0430\u0432\u044b\u043a\u043e\u0432 (\u0438\u043b\u0438 \u043a\u0440\u0438\u0432\u043e\u0440\u0443\u043a\u043e\u0441\u0442\u0438).<\/p>\n<p>  \u041a\u0430\u043a \u043d\u0430\u0441\u0447\u0435\u0442 \u0443\u0440\u043e\u0432\u043d\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u044b \u0441\u043f\u0440\u0430\u0432\u0438\u043b\u0441\u044f \u0441 \u044d\u0442\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0435\u0439? \u0421\u043c\u043e\u0433 \u0431\u044b \u00ab\u0434\u0436\u0443\u043d\u0438\u043e\u0440 \u0420\u0435\u0430\u043a\u0442-\u0434\u0435\u0432\u0435\u043b\u043e\u043f\u0435\u0440\u00bb \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043f\u043e\u0434\u043e\u0431\u043d\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435? \u00ab\u041f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u043e\u043b\u044c\u0448\u0435 \u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 \u0440\u0435\u043c\u0435\u0441\u043b\u043e\u00bb, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0431\u0435\u0437 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u041e\u041e\u041f \u0438 \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u043e\u0432, \u0434\u0443\u043c\u0430\u044e, \u044d\u0442\u043e \u0431\u044b\u043b\u043e \u0431\u044b \u0441\u043b\u043e\u0436\u043d\u043e. \u041d\u043e \u0432\u044b \u0438 \u0432\u0430\u0448\u0430 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f \u0441\u0430\u043c\u0438 \u0440\u0435\u0448\u0430\u0435\u0442\u0435, \u0432\u043e \u0447\u0442\u043e \u0432\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0435\u0442\u0435 \u0441\u0432\u043e\u0438 \u0441\u0438\u043b\u044b. \u041f\u0440\u0430\u043a\u0442\u0438\u043a\u0443\u0435\u0442\u0435\u0441\u044c \u0432 \u041e\u041e\u041f \u0438\u043b\u0438 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0432 \u0442\u043e\u043d\u043a\u043e\u0441\u0442\u044f\u0445 \u043e\u0447\u0435\u0440\u0435\u0434\u043d\u043e\u0433\u043e \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430? \u042f \u043b\u0438\u0448\u044c \u0437\u0430\u043d\u043e\u0432\u043e \u0443\u0431\u0435\u0434\u0438\u043b\u0441\u044f \u0432 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043b\u0438\u0442\u0435\u0440\u0430\u0442\u0443\u0440\u043d\u044b\u0445 \u0442\u0440\u0443\u0434\u043e\u0432 \u043e\u043f\u044b\u0442\u043d\u044b\u0445 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u043e\u0432, \u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043b, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430 \u0444\u0440\u043e\u043d\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0432\u0435\u0442\u044b \u043a\u043b\u0430\u0441\u0441\u0438\u043a\u043e\u0432 \u043d\u0430 \u043f\u043e\u043b\u043d\u0443\u044e \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c.<\/p>\n<p>  \u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0437\u0430 \u043f\u0440\u043e\u0447\u0442\u0435\u043d\u0438\u0435!<\/p><\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/491684\/\"> https:\/\/habr.com\/ru\/post\/491684\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/491684\/\">GoF, \u0427\u0438\u0441\u0442\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430, \u0421\u043e\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u2014 \u043d\u0430\u0441\u0442\u043e\u043b\u044c\u043d\u044b\u0435 \u043a\u043d\u0438\u0433\u0438 \u00abtrue \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u0430\u00bb. \u041d\u043e \u0432 \u043c\u0438\u0440\u0435 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434\u0430 \u043c\u043d\u043e\u0433\u0438\u0435 \u0438\u0434\u0435\u0438 \u0438\u0437 \u044d\u0442\u0438\u0445 \u043a\u043d\u0438\u0433 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b. \u041f\u043e \u043a\u0440\u0430\u0439\u043d\u0435\u0439 \u043c\u0435\u0440\u0435 \u0441\u0445\u043e\u0434\u0441\u0442\u0432\u043e \u0441 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c \u043c\u0438\u0440\u043e\u043c \u043d\u0430\u0439\u0442\u0438 \u043e\u0447\u0435\u043d\u044c \u0441\u043b\u043e\u0436\u043d\u043e. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434 \u043e\u043f\u0435\u0440\u0435\u0436\u0430\u0435\u0442 \u0432\u0440\u0435\u043c\u044f? \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u00ab\u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0435\u00bb \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0420\u0435\u0430\u043a\u0442 \u0443\u0436\u0435 \u0434\u043e\u043a\u0430\u0437\u0430\u043b\u0438 \u0441\u0432\u043e\u0435 \u043f\u0440\u0435\u0432\u043e\u0441\u0445\u043e\u0434\u0441\u0442\u0432\u043e \u043d\u0430\u0434 \u041e\u041e\u041f? \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0445\u043e\u0447\u0443 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043f\u0440\u0438\u043c\u0435\u0440 <a href=\"https:\/\/github.com\/justerest\/todo-list\" rel=\"nofollow\">todo-list<\/a> \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u044f \u043f\u043e\u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430\u043c \u0438 \u043f\u043e\u0434\u0445\u043e\u0434\u0430\u043c, \u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u043c \u0432 \u043a\u043b\u0430\u0441\u0441\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043a\u043d\u0438\u0433\u0430\u0445.  <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-299904","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/299904","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=299904"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/299904\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=299904"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=299904"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=299904"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}