Объектно-ориентированное программирование без классов
Июнь 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, статью в блоге и книгу "Структура и Интерпретация Компьютерных Программ".