рд╕реНрд╡рддрдВрддреНрд░ рдореЛрд░реНрдЪрд╛

GoF, рдкреНрдпреЛрд░ рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░, рдкрд░рдлреЗрдХреНрдЯ рдХреЛрдб - рдЯреНрд░реВ рдкреНрд░реЛрдЧреНрд░рд╛рдорд░ рдХреА рд╣реИрдВрдбрдмреБрдХред рд▓реЗрдХрд┐рди рдлреНрд░рдВрдЯ-рдПрдВрдб рдХреА рджреБрдирд┐рдпрд╛ рдореЗрдВ, рдЗрди рдкреБрд╕реНрддрдХреЛрдВ рдХреЗ рдХрдИ рд╡рд┐рдЪрд╛рд░ рдЙрдкрд▓рдмреНрдз рдирд╣реАрдВ рд╣реИрдВред рд╡рд╛рд╕реНрддрд╡рд┐рдХ рджреБрдирд┐рдпрд╛ рдореЗрдВ рдХрдо рд╕реЗ рдХрдо рд╕рдорд╛рдирддрд╛ рдмрд╣реБрдд рдореБрд╢реНрдХрд┐рд▓ рд╣реИред рд╢рд╛рдпрдж рдЖрдзреБрдирд┐рдХ рд╕реАрдорд╛рдВрдд рд╕рдордп рд╕реЗ рдкрд╣рд▓реЗ рд╣реИ? рд╣реЛ рд╕рдХрддрд╛ рд╣реИ рдХрд┐ "рдХрд╛рд░реНрдпрд╛рддреНрдордХ" рдкреНрд░реЛрдЧреНрд░рд╛рдорд┐рдВрдЧ рдФрд░ рд░рд┐рдПрдХреНрдЯ рдкрд╣рд▓реЗ рд╣реА рдУрдУрдкреА рдкрд░ рдЕрдкрдиреА рд╢реНрд░реЗрд╖реНрдарддрд╛ рд╕рд╛рдмрд┐рдд рдХрд░ рдЪреБрдХреЗ рд╣реЛрдВ? рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ, рдореИрдВ рдПрдХ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдг рджреЗрдирд╛ рдЪрд╛рд╣рддрд╛ рд╣реВрдВ рдЬрд┐рд╕реЗ рдореИрдВрдиреЗ рдХреНрд▓рд╛рд╕рд┐рдХ рдХрд┐рддрд╛рдмреЛрдВ рдореЗрдВ рд╡рд░реНрдгрд┐рдд рд╕рд┐рджреНрдзрд╛рдВрддреЛрдВ рдФрд░ рджреГрд╖реНрдЯрд┐рдХреЛрдгреЛрдВ рдХреЗ рдЕрдиреБрд╕рд╛рд░ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХреА рдереАред

рдврд╛рдВрдЪрд╛ рдирд┐рд░реНрднрд░рддрд╛


рд░реВрдкрд░реЗрдЦрд╛ рдЖрдзреБрдирд┐рдХ рдореЛрд░реНрдЪреЗ рдХреА рдЖрдзрд╛рд░рд╢рд┐рд▓рд╛ рд╣реИред Hh.ru рдкрд░ рд░рд┐рдХреНрддрд┐рдпрд╛рдВ рд░рд┐рдПрдХреНрдЯ рдмрдирд╛рдо рдПрдВрдЧреБрд▓рд░ рдмрдирд╛рдо рд╡реА рдбреЗрд╡рд▓рдкрд░реНрд╕ рд╣реИрдВред рдореИрдВрдиреЗ рдЗрдирдореЗрдВ рд╕реЗ рдкреНрд░рддреНрдпреЗрдХ рдЪреМрдЦрдЯреЗ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд┐рдпрд╛, рдФрд░ рдмрд╣реБрдд рд▓рдВрдмреЗ рд╕рдордп рддрдХ рдореИрдВ рдпрд╣ рдирд╣реАрдВ рд╕рдордЭ рд╕рдХрд╛ рдХрд┐ рдореБрдЭреЗ 3 рд╕рд╛рд▓ рд╕реЗ Vue рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдирд╛ рдкрдбрд╝рд╛ рдерд╛ рдПрдХ рдмрдЯрди рдХреЛ рд▓рд╛рд▓ рд╕реЗ рдмреИрдВрдЧрдиреА рд░рдВрдЧ рддрдХ? рдореБрдЭреЗ рдпрд╣ рдЬрд╛рдирдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ рдХрд┐ рдПрдХ рд╣реА рдмрдЯрди рдХреЛ рдмрд╛рдПрдВ рд╕реЗ рджрд╛рдПрдВ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкреНрд░реЛрдЯреЛрдЯрд╛рдЗрдк, рдпрд╛ рдЗрд╡реЗрдВрдЯ рд▓реВрдк рдХреЗ рд╕рд┐рджреНрдзрд╛рдВрдд рдкрд░ рдХреИрд╕реЗ рд╡рд╛рд░ рдХрд┐рдпрд╛ рдЬрд╛рдП? рдЗрд╕рдХрд╛ рдЙрддреНрддрд░ рд╕рд░рд▓ рд╣реИ - рд╣рдо рд▓рд╛рдЗрдмреНрд░реЗрд░реА-рдмрд╛рдЙрдВрдб рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд▓рд┐рдЦрддреЗ рд╣реИрдВред

рдХреНрдпреЛрдВ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рд╡рд╛рд▓реЗ рд▓рдВрдмреЗ рдЕрдиреБрднрд╡ рд╡рд╛рд▓реА рдХрдВрдкрдирд┐рдпрд╛рдВ рд╣реИрдВ? рд╣рд╛рдВ, рдХреНрдпреЛрдВрдХрд┐ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдЗрд╕ рд░рд┐рдПрдХреНрдЯ рдХреА рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдкрд░ рдЕрддреНрдпрдзрд┐рдХ рдирд┐рд░реНрднрд░ рд╣реИ, рдФрд░ рдмрдЯрди рдХреЛ рджреЛрд╣рд░рд╛рддреЗ рд╕рдордп рдХреБрдЫ рднреА рдирд╣реАрдВ рддреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдЕрдкрдиреЗ рд╕рд┐рд░ рдХреЛ рддреЛрдбрд╝рдирд╛ рдЪрд╛рд╣рд┐рдП рдХрд┐ рдХреИрд╕реЗ рдкрд░рд┐рд╡рд░реНрддрди рдХрд╛ рдкрддрд╛ рд▓рдЧрд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдШрдЯрдХ рдкреЗрдбрд╝ рдХреЗ рд░рд┐рдПрдХреНрд╢рди рдХреЗ рдЕрдВрджрд░ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИ, рдФрд░ рдпрд╣ рдмрдЯрди рдХреЛ рдлрд┐рд░ рд╕реЗ рджрдмрд╛рдиреЗ рдХреЗ рдХрд╛рд░реНрдп рд╕реЗ рдХреИрд╕реЗ рд╕рдВрдмрдВрдзрд┐рдд рд╣реИред (рдореИрдВ рдорд╛рдирддрд╛ рд╣реВрдВ, рдпреЗ рд╕рднреА рд╡рд┐рд╢реЗрд╖ рдорд╛рдорд▓реЗ рд╣реИрдВ ... рдФрд░ рдЖрдкрдХреА рдХрдВрдкрдиреА рдореЗрдВ, рдХреНрдпрд╛ рдЖрдк рдврд╛рдВрдЪреЗ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рдЕрдиреБрднрд╡ рдХреЗ рдмрд┐рдирд╛ рд╡рд┐рд╢реЗрд╖рдЬреНрдЮ рд▓реЗрдиреЗ рдХреЗ рд▓рд┐рдП рддреИрдпрд╛рд░ рд╣реИрдВ?)
рднрд╛рд╖рд╛ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реБрдП рдХрд╛рд░реНрдпрдХреНрд░рдо, рднрд╛рд╖рд╛ рдирд╣реАрдВред (рдореИрдХрдХреЛрдиреЗрд▓) рдПрдХ
рдврд╛рдВрдЪрд╛ рдПрдХ рдЙрдкрдХрд░рдг рд╣реИ, рдЬреАрд╡рди рдХрд╛ рдПрдХ рддрд░реАрдХрд╛ рдирд╣реАрдВ рд╣реИред (рдорд╛рд░реНрдЯрд┐рди)
рдореЛрд░реНрдЪреЗ рдХреА рджреБрдирд┐рдпрд╛ рдХреЗ рд▓рд┐рдП, рдпреЗ рд╢реЛрдз рд╕рдмрд╕реЗ рдЕрдЪреНрдЫрд╛ рдЦрд╛рд▓реА рд╡рд╛рдХреНрдпрд╛рдВрд╢ рд╣реИрдВ, рдФрд░ рд╡рд┐рдкрд░реАрдд рд╕рд╛рдмрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рдмрд╕реЗ рдЦрд░рд╛рдм рдЪреБрдиреМрддреА рд╣реИред рдЖрдЗрдП рдЖрдзрд┐рдХрд╛рд░рд┐рдХ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджрд╕реНрддрд╛рд╡реЗрдЬ рджреЗрдЦреЗрдВ, рдФрд░ рдПрдХ рд╕рд░рд▓ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдг рджреЗрдЦреЗрдВред

рдЖрдзрд┐рдХрд╛рд░рд┐рдХ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рд╡реЗрдмрд╕рд╛рдЗрдЯ рд╕реЗ рдЯреВрдбреВ-рд╕реВрдЪреА рдХрд╛ рдЙрджрд╛рд╣рд░рдг
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 (
      <div>
        <h3> </h3>
        <TodoList items={this.state.items} />
        <form onSubmit={this.handleSubmit}>
          <label htmlFor="new-todo">
              ?
          </label>
          <input
            id="new-todo"
            onChange={this.handleChange}
            value={this.state.text}
          />
          <button>
             #{this.state.items.length + 1}
          </button>
        </form>
      </div>
    );
  }

  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 => ({
      items: state.items.concat(newItem),
      text: ''
    }));
  }
}

class TodoList extends React.Component {
  render() {
    return (
      <ul>
        {this.props.items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    );
  }
}


тАЬ рд╕рд╣рд╛рд░рд╛ рдФрд░ рд░рд╛рдЬреНрдп рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ , рдЖрдк рдПрдХ рдЫреЛрдЯреА рд╕реА рдЯреВ-рдбреВ рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛ рд╕рдХрддреЗ рд╣реИрдВред рдЗрд╕ рдЙрджрд╛рд╣рд░рдг рдореЗрдВ, рддрддреНрд╡реЛрдВ рдХреА рд╡рд░реНрддрдорд╛рди рд╕реВрдЪреА рдХреЛ рдЯреНрд░реИрдХ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд░рд╛рдЬреНрдп рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ ... "

рдиреМрд╕рд┐рдЦрд┐рдП рдкреНрд░реЛрдЧреНрд░рд╛рдорд░ рдХреЗ рд▓рд┐рдП (рдЕрд░реНрдерд╛рдд, рдореБрдЭреЗ рдХреБрдЫ рд╕рд╛рд▓ рдкрд╣рд▓реЗ), рдпрд╣ рд╡рд╛рдХреНрдпрд╛рдВрд╢ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рдЖрдЙрдЯрдкреБрдЯ рдЙрддреНрдкрдиреНрди рдХрд░рддрд╛ рд╣реИ:" рдпрд╣рд╛рдВ рдПрдХ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХрд╛ рдПрдХ рдЖрджрд░реНрд╢ рдЙрджрд╛рд╣рд░рдг рд╣реИ "ред рд▓реЗрдХрд┐рди рдШрдЯрдХ рдореЗрдВ рд░рд╛рдЬреНрдп рдХреЛ рдХреМрди рд╕рдВрдЧреНрд░рд╣реАрдд рдХрд░рддрд╛ рд╣реИ? рдЗрд╕рдХреЗ рд▓рд┐рдП рдПрдХ рд░рд╛рдЬреНрдп рдкреНрд░рдмрдВрдзрди рдкреБрд╕реНрддрдХрд╛рд▓рдп рд╣реИред

Redux рдкреНрд░рд▓реЗрдЦрди рд╕реЗ рдПрдХ рдЯреВрдбреВ-рд╕реВрдЪреА рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдг

рд╣рд╛рдВ, рдпрд╣ рд╣реИ рдХрд┐ рдХреИрд╕реЗ рдЖрд╡реЗрджрди рдЕрдзрд┐рдХ рд╕рдордЭ рдореЗрдВ рдЖ рдЧрдпрд╛ рд╣реИ рдФрд░ рд╕рд░рд▓ (рдирд╣реАрдВ) рд╣реИ ред рдХреНрдпрд╛ рд╣рдо рдирд┐рд░реНрднрд░рддрд╛ рдХреЛ рд╕рд╣реА рджрд┐рд╢рд╛ рдореЗрдВ рдЦреАрдВрдЪрдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ?

рд╕реНрд╡рддрдВрддреНрд░ рдирд┐рд░реНрдгрдп


рдЖрдЗрдП, рдЯреВрдбреВ-рд▓рд┐рд╕реНрдЯ рдХреА рд╕рдорд╕реНрдпрд╛ рдХреЛ рдлреНрд░рдВрдЯ-рдПрдВрдб рдХреЗ рд░реВрдк рдореЗрдВ рдирд╣реАрдВ рджреЗрдЦреЗрдВ, рдЕрд░реНрдерд╛рдд, рдпрд╣ рднреВрд▓ рдЬрд╛рдПрдВ рдХрд┐ рд╣рдореЗрдВ рдПрдЪрдЯреАрдПрдордПрд▓ ("рд╡реЗрдм рдПрдХ рд╡рд┐рд╡рд░рдг рд╣реИ") рдЖрдХрд░реНрд╖рд┐рдд рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рд╣рдо рдЕрдкрдиреА рдЖрдВрдЦреЛрдВ рд╕реЗ рдкрд░рд┐рдгрд╛рдо рдХреА рдЬрд╛рдВрдЪ рдирд╣реАрдВ рдХрд░ рдкрд╛рдПрдВрдЧреЗ, рдЗрд╕рд▓рд┐рдП рд╣рдореЗрдВ рдкрд░реАрдХреНрд╖рдг рд▓рд┐рдЦрдирд╛ рд╣реЛрдЧрд╛ (рдЬреИрд╕рд╛ рдХрд┐ рдЕрдВрдХрд▓ рдмреЙрдм рдХрд╣рддреЗ рд╣реИрдВ, "рдЖрдк TDD рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ")ред рдФрд░ рдХрд╛рд░реНрдп рдХреНрдпрд╛ рд╣реИ? рдЯреВрдбреВ-рд╕реВрдЪреА рдХреНрдпрд╛ рд╣реИ? рд╣рдо рд▓рд┐рдЦрдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд░рд╣реЗ рд╣реИрдВред

Todo.spec.ts
import { Todo } from './Todo';

describe('Todo', () => {
  let todo: Todo;

  beforeEach(() => {
    todo = new Todo('description');
  });

  it('+getItems() should returns Todo[]', () => {
    expect(todo.getTitle()).toBe('description');
  });

  it('+isCompleted() should returns completion flag', () => {
    expect(todo.isCompleted()).toBe(false);
  });

  it('+toggleCompletion() should invert completion flag', () => {
    todo.toggleCompletion();
    expect(todo.isCompleted()).toBe(true);
  });
});


TodoList.spec.ts
import { TodoList } from './TodoList';

describe('TodoList', () => {
  let todoList: TodoList;

  beforeEach(() => {
    todoList = new TodoList();
  });

  it('+getItems() should returns Todo[]', () => {
    expect(todoList.getItems()).toEqual([]);
  });

  it('+add() should create item and add to collection', () => {
    todoList.add('Write tests');
    expect(todoList.getItems()).toHaveLength(1);
  });

  it('+add() should create item with the description', () => {
    const description = 'Write tests';
    todoList.add(description);
    const [item] = todoList.getItems();
    expect(item.getTitle()).toBe(description);
  });

  it('+getCompletedItems() should not returns uncompleted Todo[]', () => {
    const description = 'Write tests';
    todoList.add(description);
    expect(todoList.getCompletedItems()).toEqual([]);
  });

  it('+getCompletedItems() should returns completed Todo[]', () => {
    const description = 'Write tests';
    todoList.add(description);
    const [item] = todoList.getItems();
    item.toggleCompletion();
    expect(todoList.getCompletedItems()).toEqual([item]);
  });

  it('+getUncompletedItems() should returns uncompleted Todo[]', () => {
    const description = 'Write tests';
    todoList.add(description);
    const [item] = todoList.getItems();
    expect(todoList.getUncompletedItems()).toEqual([item]);
  });

  it('+getUncompletedItems() should not returns completed Todo[]', () => {
    const description = 'Write tests';
    todoList.add(description);
    const [item] = todoList.getItems();
    item.toggleCompletion();
    expect(todoList.getUncompletedItems()).toEqual([]);
  });
});


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;
  }
}

import { Todo } from './Todo';

export class TodoList {
  private items: Todo[] = [];

  getItems(): Todo[] {
    return this.items;
  }

  getCompletedItems(): Todo[] {
    return this.items.filter((todo) => todo.isCompleted());
  }

  getUncompletedItems(): Todo[] {
    return this.items.filter((todo) => !todo.isCompleted());
  }

  add(description: string): void {
    this.items.push(new Todo(description));
  }
}

рд╣рдореЗрдВ рд╕реВрдЪрдирд╛рддреНрдордХ рдЗрдВрдЯрд░рдлреЗрд╕ рдХреЗ рд╕рд╛рде рджреЛ рд╕рд░рд▓ рдХрдХреНрд╖рд╛рдПрдВ рдорд┐рд▓рддреА рд╣реИрдВред рдпрд╣реА рдмрд╛рдд рд╣реИ рди? рдЯреЗрд╕реНрдЯ рдкрд╛рд╕ рдХрд░рддреЗ рд╣реИрдВред рдЕрдм рд░рд┐рдПрдХреНрдЯ рдЙрдард╛рдПрдВред

import React from 'react';

import { TodoList } from './core/TodoList';

export class App extends React.Component {
  todoList: TodoList = this.createTodoList();

  render(): any {
    return (
      <React.Fragment>
        <header>
          <h1>Todo List App</h1>
        </header>
        <main>
          <TodoListCmp todoList={this.todoList}></TodoListCmp>
          <AddTodoCmp todoList={this.todoList}></AddTodoCmp>
        </main>
      </React.Fragment>
    );
  }

  private createTodoList(): TodoList {
    const todoList = new TodoList();
    todoList.add('Initial created Todo');
    return todoList;
  }
}

export const TodoListCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
  return (
    <div>
      <h2>What to do?</h2>
      <ul>
        {todoList.getItems().map((todo) => (
          <li key={todo.getTitle()}>{todo.getTitle()}</li>
        ))}
      </ul>
    </div>
  );
};

export const AddTodoCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
  return <button onClick={() => todoList.add(`Todo ${todoList.getItems().length}`)}>Add</button>;
};

рдФрд░ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░реЗрдВ рдХрд┐ ... рдХреЛрдИ рдЖрдЗрдЯрдо рдЬреЛрдбрд╝рдирд╛ рдХрд╛рдо рдирд╣реАрдВ рдХрд░рддрд╛ рд╣реИред рд╣рдореНрдо ... рдЕрдм рдпрд╣ рд╕реНрдкрд╖реНрдЯ рд╣реИ рдХрд┐ рд░рд╛рдЬреНрдп рдореЗрдВ рд╕рдм рдХреБрдЫ рдХреНрдпреЛрдВ рд▓рд┐рдЦрд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдП - рддрд╛рдХрд┐ рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЬрд╛рдирдиреЗ рдХреЗ рдмрд╛рдж рд░рд┐рдПрдХреНрдЯ рдШрдЯрдХ рдлрд┐рд░ рд╕реЗ рддреИрдпрд╛рд░ рд╣реЛ рдЬрд╛рдПред рд▓реЗрдХрд┐рди рдХреНрдпрд╛ рдпрд╣ рд╕рднреА рд╕рдВрднрд╛рд╡рд┐рдд рд╕рд┐рджреНрдзрд╛рдВрддреЛрдВ рдХрд╛ рдЙрд▓реНрд▓рдВрдШрди рдХрд░рдиреЗ рдФрд░ рджреГрд╢реНрдп рдШрдЯрдХ рдореЗрдВ рддрд░реНрдХ рд░рдЦрдиреЗ рдХрд╛ рдПрдХ рдХрд╛рд░рдг рд╣реИ? рдереЛрдбрд╝рд╛ рдзреИрд░реНрдп рдФрд░ рд╕рд╛рд╣рд╕ред рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдПрдХ рдЕрдирдВрдд рд▓реВрдк рдпрд╛ рдСрдмреНрдЬрд░реНрд╡рд░ рдкреИрдЯрд░реНрди рдореЗрдВ рдлреЛрд░реНрд╕рдЕрдкрдбреЗрдЯ () рдХреЙрд▓рд┐рдВрдЧ рдПрдХрджрдо рд╕рд╣реА рд╣реИ ред

рдореБрдЭреЗ рдЖрд░рдПрдХреНрд╕рдЬреЗ рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдкрд╕рдВрдж рд╣реИ, рд▓реЗрдХрд┐рди рдореИрдВ рдЗрд╕реЗ рдХрдиреЗрдХреНрдЯ рдирд╣реАрдВ рдХрд░реВрдВрдЧрд╛, рд▓реЗрдХрд┐рди рд╣рдорд╛рд░реЗ рдХрд╛рдо рдХреЗ рд▓рд┐рдП рдЬрд░реВрд░реА рдЗрд╕рдХреА рдПрдкреАрдЖрдИ рдХреЙрдкреА рдХрд░реЗрдВред

Observable.spec.ts
import { Observable, Subject } from './Observable';

describe('Observable', () => {
  let subject: Subject<any>;
  let observable: Observable<any>;

  beforeEach(() => {
    subject = new Subject();
    observable = subject.asObservable();
  });

  it('should call callback on next value', async () => {
    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 () => {
    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 () => {
    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<void> {
  return new Promise((resolve) => setTimeout(resolve, timeoutInMs));
}


Observable.ts
export interface Observable<T = unknown> {
  subscribe(onNext: (value: T) => void): Subscription;
}

export interface Subscription {
  unsubscribe(): void;
}

export class Subject<T = unknown> implements Observable<T> {
  protected callbackSet: Set<(value: T) => void> = new Set();

  asObservable(): Observable<T> {
    return this;
  }

  subscribe(onNext: (value: T) => void): Subscription {
    this.callbackSet.add(onNext);
    return { unsubscribe: () => this.callbackSet.delete(onNext) };
  }

  next(value: T): void {
    Promise.resolve().then(() => this.callbackSet.forEach((onNext) => onNext(value)));
  }
}


рдореЗрд░реА рд░рд╛рдп рдореЗрдВ, рдХреБрдЫ рднреА рдЬрдЯрд┐рд▓ рдирд╣реАрдВ рд╣реИред рдПрдХ рдкрд░реАрдХреНрд╖рдг рдЬреЛрдбрд╝реЗрдВ (рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреА рд╕реВрдЪрдирд╛ - рд╡рд╣ рддрд░реНрдХ рд╣реИ)ред

  it('+TodoList.prototype.add() should emit changes', async () => {
    const spy = jasmine.createSpy();
    todoList.changes.subscribe(spy);
    todoList.add('description');
    await delay();
    expect(spy).toHaveBeenCalled();
  });

рдЖрдЗрдП рдПрдХ рдкрд▓ рдХреЗ рд▓рд┐рдП рд╕реЛрдЪреЗрдВ, рд▓реЗрдХрд┐рди рдХреНрдпрд╛ рдПрдХ рдЯреЛрдбреЛ рддрддреНрд╡ рдореЗрдВ рдмрджрд▓рд╛рд╡ рд╕реЗ рдЯреЛрдбреЛрд▓рд┐рд╕реНрдЯ рдХреА рд╕реНрдерд┐рддрд┐ рдкреНрд░рднрд╛рд╡рд┐рдд рд╣реЛрддреА рд╣реИ? рдкреНрд░рднрд╛рд╡рд┐рдд рдХрд░рддрд╛ рд╣реИ - getCompletedItems / getUncompletedItems рд╡рд┐рдзрд┐рдпреЛрдВ рдХреЛ рддрддреНрд╡реЛрдВ рдХрд╛ рдПрдХ рдЕрд▓рдЧ рд╕реЗрдЯ рд╡рд╛рдкрд╕ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рд╣реЛ рд╕рдХрддрд╛ рд╣реИ рдХрд┐ рдпрд╣ рдЯреЙрдбрд▓рд┐рд╕реНрдЯ рд╡рд░реНрдЧ рдХреЛ рдЯреЙрдЧрд▓ рдХреЙрдордкреНрд▓реЗрдХреНрд╢рди рдореЗрдВ рд▓реЗ рдЬрд╛рдиреЗ рд▓рд╛рдпрдХ рд╣реЛ? рдпрд╣ рдПрдХ рдмреБрд░рд╛ рд╡рд┐рдЪрд╛рд░ рд╣реИ - рдЗрд╕ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдХреЗ рд╕рд╛рде, рд╣рдореЗрдВ рдПрдХ рдирдпрд╛ рдЯреЛрдбреЛ-рддрддреНрд╡ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рд╣рд░ рд╕реБрд╡рд┐рдзрд╛ рдХреЗ рд▓рд┐рдП рдЯреЛрдбрд▓рд┐рд╕реНрдЯ рдХреЛ рдмрдврд╝рд╛рдирд╛ рд╣реЛрдЧрд╛ (рд╣рдо рдЗрд╕ рдкрд░ рдмрд╛рдж рдореЗрдВ рд▓реМрдЯреЗрдВрдЧреЗ)ред рд▓реЗрдХрд┐рди рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдХреИрд╕реЗ рд╕реАрдЦрдирд╛ рд╣реИ, рдлрд┐рд░ рд╕реЗ рдкреНрд░реЗрдХреНрд╖рдХ? рдЪреАрдЬреЛрдВ рдХреЛ рд╕рд░рд▓ рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдЯреЛрдбреЛ-рддрддреНрд╡ рдХреЛ рдХреЙрд▓рдмреИрдХ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХрд╛ рд╕рдВрдЪрд╛рд░ рдХрд░рдиреЗ рджреЗрдВред

рдХрд╛рд░реНрдпрдХреНрд░рдо рдХрд╛ рдкреВрд░реНрдг рд╕рдВрд╕реНрдХрд░рдг рдЗрд╕ рддрд░рд╣ рджрд┐рдЦрддрд╛ рд╣реИред

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) => 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) => todo.isCompleted());
  }

  getUncompletedItems(): Todo[] {
    return this.items.filter((todo) => !todo.isCompleted());
  }

  add(description: string): void {
    this.items.push(new Todo(description, () => this.changesSubject.next({})));
    this.changesSubject.next({});
  }
}

export class App extends React.Component {
  todoList: TodoList = this.createTodoList();

  render(): any {
    return (
      <React.Fragment>
        <header>
          <h1>Todo List App</h1>
        </header>
        <main>
          <TodoListCmp todoList={this.todoList}></TodoListCmp>
          <AddTodoCmp todoList={this.todoList}></AddTodoCmp>
        </main>
      </React.Fragment>
    );
  }

  componentDidMount(): void {
    this.todoList.changes.subscribe(() => this.forceUpdate());
  }

  private createTodoList(): TodoList {
    const todoList = new TodoList();
    todoList.add('Initial created Todo');
    return todoList;
  }
}

export const TodoListCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
  return (
    <div>
      <h2>What to do?</h2>
      <ul>
        {todoList.getUncompletedItems().map((todo) => (
          <TodoCmp key={todo.id} todo={todo}></TodoCmp>
        ))}
        {todoList.getCompletedItems().map((todo) => (
          <TodoCmp key={todo.id} todo={todo}></TodoCmp>
        ))}
      </ul>
    </div>
  );
};

export const TodoCmp: React.FC<{ todo: Todo }> = ({ todo }) => (
  <li
    style={{ textDecoration: todo.isCompleted() ? 'line-through' : '' }}
    onClick={() => todo.toggleCompletion()}
  >
    {todo.getTitle()}
  </li>
);

export const AddTodoCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
  return <button onClick={() => todoList.add(`Todo ${todoList.getItems().length}`)}>Add</button>;
};

рдРрд╕рд╛ рдкреНрд░рддреАрдд рд╣реЛрддрд╛ рд╣реИ рдХрд┐ рдлреНрд░реЗрдорд╡рд░реНрдХ рд╕реЗ рд╕реНрд╡рддрдВрддреНрд░ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдХреНрдпрд╛ рджреЗрдЦрдирд╛ рдЪрд╛рд╣рд┐рдПред рдХреЗрд╡рд▓ рд╕реАрдорд╛ рдкреАрдПрд▓ рд╣реИред рдЖрдк рдХрдВрд╕реЛрд▓ рдХреЗ рд▓рд┐рдП рдкреНрд░рджрд░реНрд╢рди рдХреЛ рд▓рд╛рдЧреВ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдпрд╛ рдХреЛрдгреАрдп рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред

рд╢рд╛рдпрдж рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдХрд╛ рд╡рд░реНрддрдорд╛рди рд╕рдВрд╕реНрдХрд░рдг рдЗрддрдирд╛ рдЬрдЯрд┐рд▓ рдирд╣реАрдВ рд╣реИ рдХрд┐ рдпрд╣ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░ рд╕рдХреЗ рдХрд┐ рдПрдХ рд╕реНрд╡рддрдВрддреНрд░ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдХрд╛рдо рдХрд░ рд░рд╣рд╛ рд╣реИ рдФрд░ рдЕрдкрдиреА рддрд╛рдХрдд рдХрд╛ рдкреНрд░рджрд░реНрд╢рди рдХрд░ рд░рд╣рд╛ рд╣реИред рдЗрд╕рд▓рд┐рдП, рд╣рдо рдЕрдкрдиреА рдХрд▓реНрдкрдирд╛ рдХреЛ рдЕрдзрд┐рдХ рдпрд╛ рдХрдо рдкреНрд░рд╢рдВрд╕рдиреАрдп рдЯреВрдбреВ-рд╕реВрдЪреА рд╡рд┐рдХрд╛рд╕ рдкрд░рд┐рджреГрд╢реНрдп рд╕реЗ рдЬреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдЬреЛрдбрд╝рддреЗ рд╣реИрдВред

рдЧреНрд░рд╛рд╣рдХ рд╕реЗ рд╕рдВрдкрд╛рджрди


рдЕрдзрд┐рдХрд╛рдВрд╢ рдкрд░рд┐рдпреЛрдЬрдирд╛рдУрдВ рдХрд╛ рдореБрдЦреНрдп рджреБрдГрд╕реНрд╡рдкреНрди рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдХреЛ рдмрджрд▓ рд░рд╣рд╛ рд╣реИред рдЖрдк рдЬрд╛рдирддреЗ рд╣реИрдВ рдХрд┐ рд╕рдВрдкрд╛рджрди рд╕реЗ рдмрдЪрд╛ рдирд╣реАрдВ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ, рдФрд░ рдЖрдк рдЬрд╛рдирддреЗ рд╣реИрдВ рдХрд┐ рдпрд╣ рд╕рд╛рдорд╛рдиреНрдп рд╣реИред рд▓реЗрдХрд┐рди рдЖрдк рднрд╡рд┐рд╖реНрдп рдХреЗ рдмрджрд▓рд╛рд╡реЛрдВ рдХреЗ рд▓рд┐рдП рдХреИрд╕реЗ рддреИрдпрд╛рд░ рд╣реЛрддреЗ рд╣реИрдВ?

рд╡рд┐рд╢реЗрд╖ рдЯреЛрдбреЛ рддрддреНрд╡


OOP рдХреА рдкреНрд░рдореБрдЦ рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдореЗрдВ рд╕реЗ рдПрдХ рдирдП рдкреНрд░рдХрд╛рд░ рдХреА рд╢реБрд░реВрдЖрдд рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рд╣реИред рд╢рд╛рдпрдж рдпрд╣ рд╕рдмрд╕реЗ рд╢рдХреНрддрд┐рд╢рд╛рд▓реА рдУрдУрдкреА рддрдХрдиреАрдХ рд╣реИ рдЬреЛ рдПрдХ рдЬрдЯрд┐рд▓ рдФрд░ рдмреЛрдЭрд┐рд▓ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЛ рдПрдХрд▓ рд░реВрдк рд╕реЗ рдмрд╛рд╣рд░ рдирд┐рдХрд╛рд▓ рд╕рдХрддреА рд╣реИред рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдореБрдЭреЗ рдирд╣реАрдВ рдкрддрд╛ рдХрд┐ рдЯреЛрдбреЛ рддрддреНрд╡ рдХреА рдХреНрдпрд╛ рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рдЗрд╕рдХрд╛ рдирд╛рдо рдмрджрд▓рдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реЛ рд╕рдХрддрд╛ рд╣реИ, рдЕрддрд┐рд░рд┐рдХреНрдд рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рдЬреЛрдбрд╝рдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реЛ рд╕рдХрддрд╛ рд╣реИ, рдЗрд╕ рддрддреНрд╡ рдХреЛ рд╕реНрдкреЗрд╕рдПрдХреНрд╕ рд╕рд░реНрд╡рд░ рддрдХ рд╕реАрдзреЗ рдкрд╣реБрдВрдЪ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдмрджрд▓рдирд╛ рд╕рдВрднрд╡ рд╣реЛ рд╕рдХрддрд╛ рд╣реИ ... рд▓реЗрдХрд┐рди рдореБрдЭреЗ рдпрдХреАрди рд╣реИ рдХрд┐ рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдореЗрдВ рдмрджрд▓рд╛рд╡ рд╣реЛрдЧрд╛, рдФрд░ рдореБрдЭреЗ рд╡рд┐рднрд┐рдиреНрди рдкреНрд░рдХрд╛рд░ рдХреЗ рдЯреЛрдбреЛ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрдЧреАред

export class EditableTodo extends Todo {
  changeTitle(title: string): void {
    this.title = title;
    this.onChange?.(this);
  }
}

рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдПрдХ рд╡рд┐рд╢реЗрд╖ рдкреНрд░рдХрд╛рд░ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдореЗрдВ рджреГрд╢реНрдп рдШрдЯрдХреЛрдВ рдХреЛ рднреА рдмрджрд▓рдирд╛ рд╣реЛрдЧрд╛ред рд╡реНрдпрд╡рд╣рд╛рд░ рдореЗрдВ, рдореБрдЭреЗ рдорд┐рд▓реЗ (рдФрд░ рд▓рд┐рдЦреЗ рдЧрдП) рдШрдЯрдХ рдЬрд┐рд╕рдореЗрдВ рдПрдХ рд▓рд╛рдЦ рдЕрд▓рдЧ-рдЕрд▓рдЧ рд╕реНрдерд┐рддрд┐рдпрд╛рдВ рдЬрд┐рд░рд╛рдл рд╕реЗ рдПрдХ рдорд╢реАрди рдмреНрд▓реЙрдХ рдореЗрдВ рдПрдХ рдбрд┐рд╡ рдмреНрд▓реЙрдХ рдХреЛ рдмрджрд▓ рджреЗрддреА рд╣реИрдВред рдЗрд╕ рд╕рдорд╕реНрдпрд╛ рд╕реЗ рдмрдЪрдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдк рдПрдХ рд╡рд┐рд╢рд╛рд▓ рд╕реНрд╡рд┐рдЪ-рдХреЗрд╕ рд╕реВрдЪреА рдХреЗ рд╕рд╛рде рдПрдХ рдШрдЯрдХ рдмрдирд╛ рд╕рдХрддреЗ рд╣реИрдВред рдпрд╛ рд╡рд┐рдЬрд╝рд┐рдЯрд░ рдкреИрдЯрд░реНрди рдФрд░ рджреЛрд╣рд░реА рдкреНрд░реЗрд╖рдг рд▓рд╛рдЧреВ рдХрд░реЗрдВ , рдФрд░ рдЯреЛрдбреЛ рддрддреНрд╡ рдХреЛ рдЦреБрдж рддрдп рдХрд░рдиреЗ рджреЗрдВ рдХрд┐ рдХрд┐рд╕ рдкреНрд░рдХрд╛рд░ рдХреЗ рдШрдЯрдХ рдХреЛ рдЖрдХрд░реНрд╖рд┐рдд рдХрд░рдирд╛ рд╣реИред

export class Todo {
  id: string = '';

  constructor(
    protected title: string,
    private completed: boolean = false,
    protected onChange?: (todo: Todo) => 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 <SimpleTodoCmp todo={todo}></SimpleTodoCmp>;
  }

  renderFixedTodo(todo: Todo): any {
    return <FixedTodoCmp todo={todo}></FixedTodoCmp>;
  }

  renderEditableTodo(todo: EditableTodo): any {
    return <EditableTodoCmp todo={todo}></EditableTodoCmp>;
  }
}

рджреЛрд╣рд░реА рдкреНрд░реЗрд╖рдг рд╡рд┐рдХрд▓реНрдк рд╡рд┐рд╢реЗрд╖ рд░реВрдк рд╕реЗ рдЙрдкрдпреЛрдЧреА рд╣реЛрддрд╛ рд╣реИ рдЬрдм рдПрдХ рдкреНрд░рдХрд╛рд░ рдХреЗ рдЖрдЗрдЯрдо рдореЗрдВ рдЕрд▓рдЧ-рдЕрд▓рдЧ рд╡рд┐рдЪрд╛рд░ рд╣реЛрддреЗ рд╣реИрдВред рдЖрдк рдЙрдиреНрд╣реЗрдВ рдЕрд▓рдЧ-рдЕрд▓рдЧ рдЯреЛрдбрд░реЗрдВрдбрд░ рдХреЛ рд░реЗрдВрдбрд░ рд╡рд┐рдзрд┐ рдореЗрдВ рдмрджрд▓рдХрд░ рдмрджрд▓ рд╕рдХрддреЗ рд╣реИрдВред

рдЕрдм рд╣рдо рддреИрдпрд╛рд░ рд╣реИрдВред "рд╡рд┐рд╢реЗрд╖" рдЯреЛрдбреЛ рддрддреНрд╡реЛрдВ рдХреЗ рд▓рд┐рдП рдирдИ рдорд╛рдВрдЧреЛрдВ рдХрд╛ рдбрд░ рдЧрд╛рдпрдм рд╣реЛ рдЧрдпрд╛ рд╣реИред рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдбреЗрд╡рд▓рдкрд░реНрд╕ рдЦреБрдж рдкрд╣рд▓ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдФрд░ рдХреБрдЫ рдРрд╕реЗ рдлреАрдЪрд░реНрд╕ рдкреЗрд╢ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдЬрд┐рдирдХреЗ рд▓рд┐рдП рдирдП рдкреНрд░рдХрд╛рд░реЛрдВ рдХреА рд╢реБрд░реБрдЖрдд рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИ, рдЬреЛ рдЕрдм рдирдП рдХреЛрдб рдФрд░ рдореМрдЬреВрджрд╛ рдореЗрдВ рдиреНрдпреВрдирддрдо рдкрд░рд┐рд╡рд░реНрддрди рд▓рд┐рдЦрдХрд░ рдЬреЛрдбрд╝реЗ рдЬрд╛рддреЗ рд╣реИрдВред

рд╕рд░реНрд╡рд░ рдкрд░ рдбреЗрдЯрд╛ рдХреА рдмрдЪрдд


рд╕рд░реНрд╡рд░ рдХреЗ рд╕рд╛рде рдмрд╛рддрдЪреАрдд рдХреЗ рдмрд┐рдирд╛ рдХрд┐рд╕ рддрд░рд╣ рдХрд╛ рдЕрдиреБрдкреНрд░рдпреЛрдЧ рд╣реИ? рдмреЗрд╢рдХ, рдЖрдкрдХреЛ HTTP рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╣рдорд╛рд░реА рд╕реВрдЪреА рдХреЛ рдмрдЪрд╛рдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрдирд╛ рдЪрд╛рд╣рд┐рдП - рдПрдХ рдФрд░ рдирдИ рдЖрд╡рд╢реНрдпрдХрддрд╛ред рд╣рдо рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд░рд╣реЗ рд╣реИрдВред

AppTodoList.spec.ts
import { delay } from 'src/utils/delay';
import { TodoType } from '../core/TodoFactory';
import { AppTodoList } from './AppTodoList';

describe('AppTodoList', () => {
  let todoList: AppTodoList;

  beforeEach(() => {
    todoList = new AppTodoList({
      getItems: async () => [{ type: TodoType.Simple, title: 'Loaded todo', completed: false }],
      save: () => delay(),
    });
  });

  it('+resolve() should load saved todo items', async () => {
    await todoList.resolve();
    expect(todoList.getItems().length).toBeGreaterThan(0);
  });

  it('+resolve() should emit changes', async () => {
    const spy = jasmine.createSpy();
    todoList.changes.subscribe(spy);
    await todoList.resolve();
    await delay(1);
    expect(spy).toHaveBeenCalled();
  });

  it('+add() should emit changes', async () => {
    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 () => {
    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 () => {
    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 () => {
    const spy = jasmine.createSpy();
    todoList = new AppTodoList({ getItems: async () => [], save: async () => spy() });
    todoList.add({ title: '' });
    await delay();
    expect(spy).toHaveBeenCalled();
  });

  it('+todo.onChange() should call TodoListApi.save', async () => {
    const spy = jasmine.createSpy();
    todoList = new AppTodoList({ getItems: async () => [{ title: '' }], save: async () => 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 () => {
    const api = { getItems: async () => [], save: () => Promise.resolve() };
    todoList = new AppTodoList(api);
    todoList.add({ title: '1' });
    await delay(1);
    const savedData = JSON.stringify(todoList.getItems());
    api.save = () => Promise.reject('Mock saving failed');
    todoList.add({ title: '2' });
    expect(todoList.getItems()).toHaveLength(2);
    await delay(1);
    expect(JSON.stringify(todoList.getItems())).toBe(savedData);
  });
});


export interface TodoListApi {
  getItems(): Promise<TodoParams[]>;
  save(todoParamsList: TodoParams[]): Promise<void>;
}

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(() => this.onStateChanges());
  private synchronizedTodoParamsList: TodoParams[] = [];

  constructor(private api: TodoListApi) {}

  async resolve(): Promise<void> {
    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(() => this.onStateChanges());
    this.synchronizedTodoParamsList = todoParamsList;
    this.changesSubject.next({});
  }

  private async onStateChanges(): Promise<void> {
    this.changesSubject.next({});
    try {
      const params = this.state.getItems().map((todo) => 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);
  }
}

рд╣рдореЗрдВ рдирд╣реАрдВ рдкрддрд╛ рдХрд┐ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдХреИрд╕реЗ рд╡реНрдпрд╡рд╣рд╛рд░ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред рд╕рдлрд▓ рд╣реЛрдиреЗ рдФрд░ рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рд╣реЗрдЬреЗрдВ рдХреА рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░реЗрдВ? рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрдВ, рдФрд░ рддреНрд░реБрдЯрд┐ рдХреЗ рдорд╛рдорд▓реЗ рдореЗрдВ, рд╕рд┐рдВрдХреНрд░рдирд╛рдЗрдЬрд╝ рд░рд╛рдЬреНрдп рдореЗрдВ рд╡рд╛рдкрд╕ рдЬрд╛рдПрдВ? рдпрд╛ рддреНрд░реБрдЯрд┐рдпреЛрдВ рдХреЛ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рдЕрдирджреЗрдЦрд╛ рдХрд░реЗрдВ? рд╕рдмрд╕реЗ рдЕрдзрд┐рдХ рд╕рдВрднрд╛рд╡рдирд╛ рд╣реИ, рдЧреНрд░рд╛рд╣рдХ рдХреЛ рднреА рдпрд╣ рдкрддрд╛ рдирд╣реАрдВ рд╣реИред рдЗрд╕рд▓рд┐рдП, рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдореЗрдВ рдкрд░рд┐рд╡рд░реНрддрди рдЕрдкрд░рд┐рд╣рд╛рд░реНрдп рд╣реИ, рд▓реЗрдХрд┐рди рдЙрдиреНрд╣реЗрдВ рд╕рдВрд░рдХреНрд╖рдг рдХреЗ рд▓рд┐рдП рдЬрд┐рдореНрдореЗрджрд╛рд░ рдХреЗрд╡рд▓ рдПрдХ рд╡рд░реНрдЧ рдХреЛ рдкреНрд░рднрд╛рд╡рд┐рдд рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред рдФрд░ рд░рд╛рд╕реНрддреЗ рдореЗрдВ рдЕрдЧрд▓рд╛ рдПрдбрд┐рдЯред

рдЗрддрд┐рд╣рд╛рд╕ рдмрджрд▓реЗрдВ


"рд╣рдореЗрдВ рдХрд╛рд░реНрд░рд╡рд╛рдИ / рд░рджреНрдж рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ" ...

рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдирдП рд╕рдВрдкрд╛рджрди рдХреА рдПрдХ рд▓рд╣рд░ рд╣рдореЗрдВ рдЖрд╢реНрдЪрд░реНрдпрдЪрдХрд┐рдд рдХрд░рддреА рд╣реИред рд▓реЗрдХрд┐рди рдХрд┐рд╕реА рднреА рдорд╛рдорд▓реЗ рдореЗрдВ, рдЖрдк рдкрд░реАрдХреНрд╖рдгреЛрдВ рдХрд╛ рддреНрдпрд╛рдЧ рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗред рдЕрдм рдпрд╣ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рд╕реНрдкрд╖реНрдЯ рдирд╣реАрдВ рд╣реИ рдХрд┐ рд╡рдВрд╢рд╛рдиреБрдХреНрд░рдо рдкрджрд╛рдиреБрдХреНрд░рдо рдмреЗрд╣рддрд░ рдЕрдиреБрдХреВрд▓ рд╣реИ, рдФрд░ рдХреНрдпрд╛ рд╡рд┐рд░рд╛рд╕рдд рдЖрдорддреМрд░ рдкрд░ рдЙрдкрдпреБрдХреНрдд рд╣реИред рдЗрд╕рд▓рд┐рдП, рдХреБрдЫ рднреА рдмреБрд░рд╛ рдирд╣реАрдВ рд╣реЛрдЧрд╛ рдпрджрд┐ рд╣рдо рдХреЗрд╡рд▓ рд╣рдорд╛рд░реЗ рдЧрдВрджреЗ рд╡рд░реНрдЧ рдХреЛ рдкреВрд░рдХ рдХрд░рддреЗ рд╣реИрдВ (рд╣рдо рдЗрд╕реЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╡рд┐рд╡рд░рдг рдкрд░ рд╡рд┐рдЪрд╛рд░ рдХрд░реЗрдВрдЧреЗ), рдПрдХрдорд╛рддреНрд░ рдЬрд┐рдореНрдореЗрджрд╛рд░реА рдХреЗ рд╕рд┐рджреНрдзрд╛рдВрдд рдХрд╛ рддреНрдпрд╛рдЧ рдХрд░рддреЗ рд╣реБрдПред

TodoListHistory.spec.ts
import { TodoParams } from 'src/core/TodoFactory';
import { delay } from 'src/utils/delay';
import { TodoListHistory } from './TodoListHistory';

describe('TodoListHistory', () => {
  let history: TodoListHistory;

  beforeEach(() => {
    history = new TodoListHistory();
  });

  it('+getState() should returns TodoParams[]', () => {
    expect(history.getState()).toEqual([]);
  });

  it('+setState() should rewrite current state', () => {
    const newState = [{ title: '' }] as TodoParams[];
    history.setState(newState);
    expect(history.getState()).toBe(newState);
  });

  it('+hasPrev() should returns false on init', () => {
    expect(history.hasPrev()).toBe(false);
  });

  it('+hasPrev() should returns true after setState()', () => {
    history.setState([]);
    expect(history.hasPrev()).toBe(true);
  });

  it('+switchToPrev() should switch on prev state', () => {
    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', () => {
    history.setState([]);
    history.switchToPrev();
    expect(history.hasPrev()).toBe(false);
  });

  it('+hasNext() should returns false on init', () => {
    expect(history.hasNext()).toBe(false);
  });

  it('+hasNext() should returns true after switchToPrev()', () => {
    history.setState([]);
    history.switchToPrev();
    expect(history.hasNext()).toBe(true);
  });

  it('+switchToNext() should switch on next state', () => {
    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()', () => {
    history.setState([]);
    history.switchToPrev();
    history.switchToNext();
    expect(history.hasNext()).toBe(false);
  });

  it('+hasNext() should returns false after setState()', () => {
    history.setState([]);
    history.switchToPrev();
    history.setState([]);
    expect(history.hasNext()).toBe(false);
  });

  it('+switchToPrev() should switch on prev state after setState()', () => {
    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 () => {
    const spy = jasmine.createSpy();
    history.changes.subscribe(spy);
    history.setState([]);
    await delay();
    expect(spy).not.toHaveBeenCalled();
  });

  it('+switchToPrev() should emit changes', async () => {
    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 () => {
    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 () => {
    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);
  });
});


AppTodoList.spec.ts
import { delay } from 'src/utils/delay';
import { TodoType } from '../core/TodoFactory';
import { AppTodoList } from './AppTodoList';

describe('AppTodoList', () => {
  let todoList: AppTodoList;

  beforeEach(() => {
    todoList = new AppTodoList({
      getItems: async () => [{ type: TodoType.Simple, title: 'Loaded todo', completed: false }],
      save: () => delay(),
    });
  });

  it('+resolve() should load saved todo items', async () => {
    await todoList.resolve();
    expect(todoList.getItems().length).toBeGreaterThan(0);
  });

  it('+resolve() should emit changes', async () => {
    const spy = jasmine.createSpy();
    todoList.changes.subscribe(spy);
    await todoList.resolve();
    await delay();
    expect(spy).toHaveBeenCalled();
  });

  it('+add() should emit changes', async () => {
    const spy = jasmine.createSpy();
    todoList.changes.subscribe(spy);
    todoList.add({ title: '' });
    await delay();
    expect(spy).toHaveBeenCalled();
  });

  it('+add() should emit changes after resolve()', async () => {
    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 () => {
    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 () => {
    const spy = jasmine.createSpy();
    todoList = new AppTodoList({ getItems: async () => [], save: async () => spy() });
    todoList.add({ title: '' });
    await delay();
    expect(spy).toHaveBeenCalled();
  });

  it('+todo.onChange() should call TodoListApi.save', async () => {
    const spy = jasmine.createSpy();
    todoList = new AppTodoList({ getItems: async () => [{ title: '' }], save: async () => spy() });
    await todoList.resolve();
    const [todo] = todoList.getItems();
    todo.toggleCompletion();
    await delay();
    expect(spy).toHaveBeenCalled();
  });

  it('+add() should ignore error on save', async () => {
    const api = { getItems: async () => [], save: () => 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 () => {
    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 () => {
    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 () => {
    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 () => {
    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 () => {
    todoList.add({ title: '' });
    todoList.getHistory().switchToPrev();
    await delay();
    const spy = jasmine.createSpy();
    todoList.changes.subscribe(spy);
    todoList.add({ title: '' });
    await delay();
    expect(spy).toHaveBeenCalled();
  });
});


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() > 0;
  }

  hasNext(): boolean {
    return this.getCurrentStateIndex() < 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(() =>
    this.onStateChanges(),
  );
  private historySubscription = this.history.changes.subscribe(() => this.onHistoryChanges());

  constructor(private api: TodoListApi) {}

  private onStateChanges(): void {
    const params = this.state.getItems().map((todo) => this.todoFactory.serializeTodo(todo));
    this.history.setState(params);
    this.api.save(params).catch(() => {});
    this.changesSubject.next({});
  }

  private onHistoryChanges(): void {
    const params = this.history.getState();
    this.updateStateTodoList(params);
    this.api.save(params).catch(() => {});
  }

  private updateStateTodoList(todoParamsList: TodoParams[]): void {
    const todoList = new TodoListImp(todoParamsList);
    this.state = todoList;
    this.stateSubscription.unsubscribe();
    this.stateSubscription = this.state.changes.subscribe(() => this.onStateChanges());
    this.changesSubject.next({});
  }

  async resolve(): Promise<void> {
    const todoParamsList = await this.api.getItems();
    this.history.reset(todoParamsList);
    this.updateStateTodoList(todoParamsList);
  }

  destroy(): void {
    this.stateSubscription.unsubscribe();
    this.historySubscription.unsubscribe();
  }

  getHistory(): HistoryControl<TodoParams[]> {
    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);
  }
}

рдЪреВрдВрдХрд┐ рдкрд░рд┐рд╡рд░реНрддрди рдЗрддрд┐рд╣рд╛рд╕ рдмрд╣реБрдд рд╕рдорд╛рди рд╣реИ, рдЗрд╕рд▓рд┐рдП рдЯреВрдбреВ-рд╕реВрдЪреА рдЗрддрд┐рд╣рд╛рд╕ рдХреЗ рдкреНрд░рдмрдВрдзрди рдХреЛ рдЖрдзрд╛рд░ рд╡рд░реНрдЧ рдореЗрдВ рдЕрд▓рдЧ рдХрд░реЗрдВред

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<TodoParams[]>([]);

  private changesSubject: Subject = new Subject();
  readonly changes: Observable = this.changesSubject.asObservable();

  private state: TodoList = new TodoListImp();
  private stateSubscription: Subscription = this.state.changes.subscribe(() =>
    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(() =>
      this.onStateChanged(this.getSerializedState()),
    );
  }

  private getSerializedState(): TodoParams[] {
    return this.state.getItems().map((todo) => 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<void> {
    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(() => this.undo());
  }

  protected onHistorySwitched(): void {
    super.onHistorySwitched();
    this.api.save(this.history.getState()).catch(() => {});
  }
}

рд╕рдВрд░рдХреНрд╖рдг рд╕рдорд╕реНрдпрд╛ рдХрд╛ рд╕рдорд╛рдзрд╛рди рдЦреБрдж рд╣реА рдЖ рдЧрдпрд╛ред рдЕрдм рд╣рдо рдЗрд╕ рдмрд╛рд░реЗ рдореЗрдВ рдЪрд┐рдВрддрд╛ рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рдЧреНрд░рд╛рд╣рдХ рд▓рдВрдмреА рдЕрд╡рдзрд┐ рдореЗрдВ рдХрд┐рд╕ рд╕рдВрд░рдХреНрд╖рдг рд░рдгрдиреАрддрд┐ рдХрд╛ рдЪрдпрди рдХрд░реЗрдВрдЧреЗред рдмреЗрд╕ рдХреНрд▓рд╛рд╕ рдХрд╛ рд╡рд┐рд╕реНрддрд╛рд░ рдХрд░рддреЗ рд╣реБрдП, рдЖрдк рдЙрд╕реЗ рдЪреБрдирдиреЗ рдХреЗ рд▓рд┐рдП рд╕рднреА 3 рд╡рд┐рдХрд▓реНрдк рдкреНрд░рджрд╛рди рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред

рд╕рд╛рд░рд╛рдВрд╢


рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рд╣рдорд╛рд░реА рдЯреВрдбреВ рд╕реВрдЪреА рд╕реЗ рд╣рдореЗрдВ Google Keep рдХрд╛ рдПрдХ рдЫреЛрдЯрд╛ рдкреНрд░реЛрдЯреЛрдЯрд╛рдЗрдк рдорд┐рд▓ рдЧрдпрд╛ рд╣реИред рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд▓реЙрдиреНрдЪ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рд╕реЗ рд▓рд┐рдВрдХ рдХрд░реЗрдВ рдпрд╛ рдХрдорд┐рдЯ рдХреЗ рдЗрддрд┐рд╣рд╛рд╕ рдкрд░ рдЬрд╛рдПрдВред

рдЗрд╕ рдЙрджрд╛рд╣рд░рдг рдХреЛ рдЬреНрдпрд╛рджрд╛рддрд░ рдлреНрд░рдВрдЯ-рдПрдВрдб рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╕реЗ рдореМрд▓рд┐рдХ рд░реВрдк рд╕реЗ рдХреНрдпрд╛ рдЕрд▓рдЧ рдХрд░рддрд╛ рд╣реИ? рд╣рдо рдкреБрд╕реНрддрдХрд╛рд▓рдпреЛрдВ рдкрд░ рдирд┐рд░реНрднрд░ рдирд╣реАрдВ рдереЗ, рдЗрд╕рд▓рд┐рдП, рдПрдХ рд╡реНрдпрдХреНрддрд┐ рдЬрд┐рд╕рдиреЗ рдХрднреА рднреА рд░рд┐рдПрдХреНрдЯ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдирд╣реАрдВ рдХрд┐рдпрд╛ рд╣реИ, рд╡рд╣ рдЗрд╕ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд╕рдордЭ рд╕рдХрддрд╛ рд╣реИред рд╣рдорд╛рд░реЗ рдирд┐рд░реНрдгрдп рдХреЗрд╡рд▓ рдкрд░рд┐рдгрд╛рдо рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдХреЗ рдЙрджреНрджреЗрд╢реНрдп рд╕реЗ рдереЗ, рдлреНрд░реЗрдорд╡рд░реНрдХ рдХреЗ рд╡рд┐рд╡рд░рдг рдкрд░ рдзреНрдпрд╛рди рднрдВрдЧ рдХрд┐рдП рдмрд┐рдирд╛, рдЗрд╕рд▓рд┐рдП рдХреЛрдб рдХрдо рдпрд╛ рдЬреНрдпрд╛рджрд╛ рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рддрд╛ рд╣реИред рд╣рдо рдирдП рдкреНрд░рдХрд╛рд░ рдХреЗ рдЯреЛрдбреЛ рддрддреНрд╡реЛрдВ рдХреЛ рдЬреЛрдбрд╝рдирд╛ рдЖрд╕рд╛рди рдмрдирд╛рдиреЗ рдореЗрдВ рдХрд╛рдордпрд╛рдм рд░рд╣реЗ, рдФрд░ рд╣рдо рд╕рдВрд░рдХреНрд╖рдг рд░рдгрдиреАрддрд┐ рдХреЛ рдмрджрд▓рдиреЗ рдХреЗ рд▓рд┐рдП рддреИрдпрд╛рд░ рд╣реИрдВред

рд╣рдореЗрдВ рдХрд┐рди рдХрдард┐рдирд╛рдЗрдпреЛрдВ рдХрд╛ рд╕рд╛рдордирд╛ рдХрд░рдирд╛ рдкрдбрд╝рд╛? рд╣рдордиреЗ рдлреНрд░реЗрдорд╡рд░реНрдХ рдХреЗ рд╕рдВрджрд░реНрдн рдХреЗ рдмрд┐рдирд╛ рдСрдмреНрдЬрд░реНрд╡рд░ рдкреИрдЯрд░реНрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рджреГрд╢реНрдп рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдХреА рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд┐рдпрд╛ред рдЬреИрд╕рд╛ рдХрд┐ рдпрд╣ рдирд┐рдХрд▓рд╛, рдЗрд╕ рдкреИрдЯрд░реНрди рдХреЗ рдЖрд╡реЗрджрди рдХреЛ рдЕрднреА рднреА рдореБрдЦреНрдп рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдереА (рднрд▓реЗ рд╣реА рд╣рдореЗрдВ HTML рдЦреАрдВрдЪрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рдереА)ред рдЗрд╕рд▓рд┐рдП, рд╣рдордиреЗ рдлреНрд░реЗрдорд╡рд░реНрдХ рдореЗрдВ рдирд┐рд░реНрдорд┐рдд рдкрд░рд┐рд╡рд░реНрддрди рдХрд╛ рдкрддрд╛ рд▓рдЧрд╛рдиреЗ рд╡рд╛рд▓реА рдкреНрд░рдгрд╛рд▓реА рдХреА "рд╕реЗрд╡рд╛рдУрдВ" рдХреЛ рдЫреЛрдбрд╝ рдХрд░ рд▓рд╛рдЧрдд рдХрд╛ рдЕрдиреБрдорд╛рди рдирд╣реАрдВ рд▓рдЧрд╛рдпрд╛ рдерд╛ред

рдореИрдВ рдЗрд╕ рдмрд╛рдд рдкрд░ рдЬреЛрд░ рджреЗрдирд╛ рдЪрд╛рд╣реВрдВрдЧрд╛ рдХрд┐ рд▓реЗрдЦрди рдкрд░реАрдХреНрд╖рдг рдореЗрдВ рдХреЛрдИ рдХрдард┐рдирд╛рдИ рдирд╣реАрдВ рдереАред рдПрдХ рд╕реВрдЪрдирд╛рддреНрдордХ рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдХреЗ рд╕рд╛рде рд╕рд░рд▓ рд╕реНрд╡рддрдВрддреНрд░ рд╡рд╕реНрддреБрдУрдВ рдХрд╛ рдкрд░реАрдХреНрд╖рдг рдПрдХ рдЦреБрд╢реА рд╣реИред рдХреЛрдб рдХреА рдЬрдЯрд┐рд▓рддрд╛ рдХреЗрд╡рд▓ рдХрд╛рд░реНрдп рдФрд░ рдореЗрд░реЗ рдХреМрд╢рд▓ (рдпрд╛ рд╡рдХреНрд░рддрд╛) рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░рддреА рдереАред

рдбреЗрд╡рд▓рдкрд░ рдХреЗ рд╕реНрддрд░ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдХреНрдпрд╛ рдЬреЛ рдЗрд╕реЗ рд╕рдВрднрд╛рд▓реЗрдВрдЧреЗ? рдХреНрдпрд╛ рдЬреВрдирд┐рдпрд░ рд░рд┐рдПрдХреНрдЯрд░ рдбреЗрд╡рд▓рдкрд░ рдРрд╕рд╛ рд╕рдорд╛рдзрд╛рди рд▓рд┐рдЦ рд╕рдХрддрд╛ рд╣реИ? "рдкреНрд░реЛрдЧреНрд░рд╛рдорд┐рдВрдЧ рдПрдХ рд╢рд┐рд▓реНрдк рдХреА рддрд░рд╣ рдЕрдзрд┐рдХ рд╣реИ", рдЗрд╕рд▓рд┐рдП рдУрдУрдкреА рдФрд░ рдкреИрдЯрд░реНрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреЗ рдЕрднреНрдпрд╛рд╕ рдХреЗ рдмрд┐рдирд╛, рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдпрд╣ рдореБрд╢реНрдХрд┐рд▓ рд╣реЛрдЧрд╛ред рд▓реЗрдХрд┐рди рдЖрдк рдФрд░ рдЖрдкрдХреА рдХрдВрдкрдиреА рддрдп рдХрд░рддреА рд╣реИ рдХрд┐ рдЖрдк рдХрд┐рд╕рдореЗрдВ рдирд┐рд╡реЗрд╢ рдХрд░рддреЗ рд╣реИрдВред рдХреНрдпрд╛ рдЖрдк OOP рдХрд╛ рдЕрднреНрдпрд╛рд╕ рдХрд░рддреЗ рд╣реИрдВ рдпрд╛ рдЕрдЧрд▓реЗ рдврд╛рдВрдЪреЗ рдХреА рдкреЗрдЪреАрджрдЧрд┐рдпреЛрдВ рдХреЛ рд╕рдордЭрддреЗ рд╣реИрдВ? рдореИрдВ рдХреЗрд╡рд▓ рдЕрдиреБрднрд╡реА рдкреНрд░реЛрдЧреНрд░рд╛рдорд░реЛрдВ рдХреЗ рд╕рд╛рд╣рд┐рддреНрдпрд┐рдХ рдХрд╛рд░реНрдпреЛрдВ рдХреА рдкреНрд░рд╛рд╕рдВрдЧрд┐рдХрддрд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЖрд╢реНрд╡рд╕реНрдд рд╣реЛ рдЧрдпрд╛, рдФрд░ рдпрд╣ рджрд┐рдЦрд╛рдпрд╛ рдХрд┐ рд╕рд╛рдордиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдкрд░ рдХреНрд▓рд╛рд╕рд┐рдХреНрд╕ рдХреА рд╕рд▓рд╛рд╣ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХреИрд╕реЗ рд╢реБрд░реВ рдХрд░реЗрдВред

рдкрдврд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдзрдиреНрдпрд╡рд╛рдж!

All Articles