Заметки Романа Теличкина

Объектно-ориентированное программирование без классов

Июнь 2018
В современных объектно-ориентированных языках программирования фокус смещен с объектов и их общения на классы и типы данных. Но ООП можно реализовывать без классов, типов и даже без формальных объектов! По-сути объект является набором поведений со скрытым внутренним состоянием. Поведения – это те сообщения (методы) на которые объект может отвечать. Внутреннее состояние – это внутренняя кухня объекта, о которой никто снаружи не знает. 
И набор поведений, и скрытое внутреннее состояние можно реализовать в любом языке программирования, который поддерживает first-class функции, замыкания и переопределение переменных. На языке JavaScript определение объекта без использования формального JavaScript-объекта будет выглядеть так:
function Note(title, text) {
  function self(method) {
    switch (method) {
      case 'asString': return () => {
        return `${title}\n\n${text}`;
      };
      case 'changeTitle': return (newTitle) => {
        title = newTitle;
        return self('asString')();
      }
    }
  }
  return self;
}

const note = Note('About OOP', 'Some text');
note('asString')();
/*
"About OOP
Some text"
*/

note('changeTitle')('OOP without classes');
/*
"OOP without classes
Some text"
*/
Получился отличный объект: он предоставляет набор поведений, а клиент этого объекта может влиять на внутреннее только через сообщения. Создать приватный метод в таком объекте – раз плюнуть:
function Note(title, text) {
  function self (method) {
    switch (method) {
      case 'asString': return format;
      case 'changeTitle': return (newTitle) => {
        title = newTitle;
        return format();
      }
    }
  }

  function format() {
    return `${title}\n\n${text}`;
  }
  return self;
}

const note = Note('About OOP', 'Some text');
note('asString')();
note('changeTitle')('OOP without classes');
Такой подход создания объектов можно сделать намного дружелюбней, заменив switch-выражение на формальный JavaScript-объект. Не используя ни class, ни new, ни this, мы создаем прекрасные объекты:
function Note(title, text) {
  const self = {
    asString: format,
    changeTitle: (newTitle) => {
      title = newTitle;
      return format();
    }
  };

  function format() { return `${title}\n\n${text}`; }

  return self;
}

const note = Note('About OOP', 'Some text');
note.asString();
note.changeTitle('OOP without classes');
На самом деле, для создания объектно-ориентированных систем в наследовании нет необходимости. Большинство задач можно решить с помощью композиции, но если очень хочется использовать наследование, то это тоже возможно:
function Superman(name, age) {
  const man = Man(name, age);
  const self = {
    whoAreYou: () => `I'm a ${age} years old superman!`,
  }
  return Object.assign({}, man, self);
}

function Man(name, age) {
  const self = {
    whoAreYou: () => `I'm a ${age} years old man`,
    whatIsYourName: () => `My name is ${name}`,
  }
  return self;
}

const superman = Superman('Jack', 32);
const man = Man('Alex', 30);

superman.whoAreYou();
// "I'm a 32 years old superman!"
man.whoAreYou();
// "I'm a 30 years old man"
superman.whatIsYourName();
// "My name is Jack"
man.whatIsYourName();
// "My name is Alex"
Последние два сниппета кода формально являются фабриками JavaScript-объектов, которые содержат в себе только методы и не дают напрямую управлять внутренним состоянием объекта и его приватными методами. По-сути же эти два сниппета – пример хорошего описанием объекта без использования class, new и this. Проще – лучше!
Если вам интересна тема ООП и его отличия от абстрактных типов данных, предлагаю почитать хороший paper, статью в блоге и книгу "Структура и Интерпретация Компьютерных Программ".
Открыть комментарии