Недавно спросили, могу ли на примерах обучить объектно-ориентированному программированию (ООП), что удивительно. Ведь существует так много материала по данному вопросу: видео, литература, интерактивные сайты и множество других источников, но задумался.
Ниже изложен результат размышления начиная с ответа на вопрос «Кто клиент программы?», до отчасти вымышленной истории появления ООП и в итоге вывод, что ж за толк разработчикам от использования ООП.
Кто является клиентом программы?
Программа обернута треугольником требований: компьютером, пользователем, разработчиком. Кто из них является клиентом программы?
Рис. 1 Три клиента программы: компьютер, пользователь, программист
Так для кого пишется софт?
Первично — пользователь и именно он выбирает и идет на компромисс:
— Делать вручную
— Простое ПО, мало возможностей
— Сложное ПО — мощный комп или длительное время ожидания
В прошлом вычислительные возможности компьютеров были малы и сложный, крупный софт мало кого интересовал, и, если создавался, то различными корпорациями и консорциумами. Программы, создаваемые рядовыми разработчиками, были просты, малы в объемах и «затачивались» под определенное железо.
Рис. 2 Требовательный компьютер, но недовольный пользователь
Сейчас же рынок ПО насыщен и пользователь хочет сложной логики, интерактивности, графики и самое удивительное — это часто возможно!
Рис. 3 Мощный компьютер, довольный пользователь
Вот только беда — софт стал еще крупнее, сложнее, и для разработки сколько-нибудь серьезного проекта требуется команда.
Рис. 4 Взаимодействие множества программистов через один программный код
Возникает взаимодействие Программист-программист, причем с развитостью сети, порою это взаимодействие только через программный код. Какое требование появляется? Правильно! Умение понимать чужой код и писать его, чтоб он был понятен, прост для других.
Как добиться простоты?
1. Взять простую задачу;
2. Разбить сложную на простые. С возможностью осознать логику разбиения;
3. Уменьшить количество взаимосвязей;
4. Очертить ответственность (логику разбиения);
5. Следить за эффективностью трудозатрат на изменение (убираем дублирование);
6. Быть уверенным, что программа делает то, что нужно — тестирование.
Вернемся к ООП
ООП базируется на трех основных принципах: инкапсуляция, наследование и полиморфизм.
Инкапсуляция — если вы спросите в меру разбирающегося программиста: «Что такое инкапсуляция?», то практически в 100% случаев услышите, что это сокрытие кодов и данных. Но есть немаловажная вещь: объединение того и другого, сужение контекста.
Представьте, что вы программируете на структурном языке, к примеру С. И нужен вам двусвязный список. Получится код, подобный приведенному ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#define TYPE int typedef struct _listNode { struct _listNode* prevNode; struct _listNode* nextNode; TYPE value; } listNode; typedef struct _list{ listNode* firstNode; listNode* lastNode; listNode* thisNode; } list; void listAddNode(list* parentList, TYPE newValue); void freeList(list* parentList); unsigned long listLength(list* parentList); bool find(list* parentList, TYPE key); |
Обратите внимание: какое слово повторяется в имени функций? Правильно — list, с помощью него задается контекст. Также у нас есть методы добавить, очистить, найти. Представьте, что мы объявили переменную типа list и хотим добавить в нее элементы, при данном действии в списке возможных функций вылезает громадное количество функций для добавления в словарь, хэш-таблицу и еще много чего. Вы возразите, что есть пространства имен и с помощью них можно задать контекст, но пользоваться конструкциями list::add(employeeList, newEmployee); не удобно, особенно с учетом использования длинных имен в крупных проектах, когда количество типов коллекций переваливает за десяток.
В ООП объект List прекрасно определяет контекст и методы, которыми можно изменять его состояние. Написал имя переменной, нажал Ctrl+пробел — наслаждаешься кратким списком возможностей работы с данным объектом. Благодаря данному свойству ООП даже и не припомню, когда последний раз смотрел в документацию для ознакомления с возможностями класса.
Сокрытие данных и кода обрабатывающего эти данные еще один плюс. Он экономит ресурс нашего восприятия (мыслетоплево) для ознакомления с классом — посмотрели на открытые методы и знаем, что мы можем сделать с объектом класса, не надо разбираться во внутренностях, только если там ошибка или надо поменять логику.
Наследование, полиморфизм — что устраняет? Как считаете? На мой взгляд дублирование. Есть схожие сущности в предметной области, отличающиеся парой дополнительных полей и чуть-чуть другой обработкой этих данных. Как быть? В структурных языках данная задача решалась через неразмеченные или заранее зарезервированные области памяти с указателем void на них и указателями на функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
typedef struct salaryInputData { char* Name; int workHoursCount; double workHourRate; int memory[8]; void *calculate; } SalaryInput; typedef double (*SalaryCalculate)(const SalaryInput &salaryInput); double baseSalaryCalculate(const SalaryInput &salaryInput) { double result = salaryInput.workHoursCount * salaryInput.workHourRate; return result; } double SalaryCalculateSales(const SalaryInput &salaryInput) { double result = baseSalaryCalculate(salaryInput); double salesProcent = (double)*(salaryInput.memory); double salesSum = (double)*(salaryInput.memory + 1); result += salesProcent * salesSum; return result; } double SalaryCalculateProgrammist(const SalaryInput &salaryInput) { double result = baseSalaryCalculate(salaryInput); int workHoursPlanSpeed = salaryInput.memory[2]; if (salaryInput.workHoursCount > workHoursPlanSpeed) { double premium = *(double*)salaryInput.memory; result += premium; } return result; } |
Налицо, что в данном коде очень легко запутаться. В нужный момент в выделенной памяти может оказаться неожиданное значение.
1 2 3 4 5 6 7 8 |
for (int i = 0; i < employeesCount; i++) { SalaryInput &employee = employees[i]; double wageSum = ((SalaryCalculate)employee.calculate)(employee); printf("%s %0.2lf", employee.Name, wageSum); } |
Подведем итог
ООП — это синтаксический сахар, основанный на базовых принципах:
— сужение контекста,
— самодокументированние кода,
— устранение дублирования.
Этот сахар здорово подслащивает жизнь при разработке больших и сложных проектов.
P.S. Именно в обсуждении ООП с ребятами из моей команды всплыл момент, про который я забыл создавая данный материал — ООП дает терминологию, близкую к терминам предметной области, мы мыслим, говорим, пишем код терминами предметной области, что позволяет общаться вам на одном языке с заказчиками и командой (Абстракция).
ООП не панацея, у любого инженерного решения есть свои плюсы, минусы, и научиться их видеть можно лишь с опытом. В данной статье приведено мое видение ООП, принципов программирования. Это то, что позволяет мне, как программисту, строить системы, которые работают у Заказчика и воспринимаются другими программистами. Напишите в комментариях насколько была полезна, интересна для вас статья.