Как написать действительно объектно-ориентированную программу
Когда я начал проводить собеседования с новыми кандидатами в нашу команду, я понял, что объектно-ориентированное программирование - тема, которая на самом деле не совсем понятна большинству людей. На вопрос: «Знаете ли вы, что такое объектно-ориентированное программирование?», я обычно получал положительный ответ. На следующий вопрос: «Не могли бы вы вкратце рассказать мне, что это?» я никогда не получал дважды один и тот же ответ. Я получил много хороших ответов и много ответов, которые не совсем соответствовали моему пониманию. И в какой-то момент я начал думать о том, как бы я ответил на этот вопрос, и понял, что это совсем не просто.
Был еще один пример: мне нужно было повторно использовать уже существующий код C ++ для моего компонента. В основном это был один класс, содержащий около 200 функций и 100 переменных, все они были открытыми, и было довольно непонятно, что они выражают. После анализа я просто понял, что не могу использовать его как есть по нескольким причинам:
- Он использовал сторонний логгер, который я не хотел включать в свой проект.
- Программа, которая использовала его первоначально, была основана на связи TCP/IP. Большинство функций вызывали исключения, если связь TCP не была настроена должным образом, даже если они не имели к этому никакого отношения.
- Некоторые переменные-члены использовались в нескольких функциях с совершенно разными целями. Поэтому, если вы хотите вызывать функцию-член извне, сначала вам нужно установить около 10 переменных-членов с правильными значениями, чтобы иметь возможность получить некоторые результаты от функции.
Учитывая все вышесказанное, я просто решил переделать этот кусок кода перед его повторным использованием, и мне потребовалось много времени, чтобы понять, как это сделать, потому что многие функции нужно было выделить в отдельный класс, и его нужно было вычислить. какие эффекты производят каждая переменная для разных функций. Это был кошмар. Когда клиент спросил меня, зачем нужен этот рефакторинг, я просто ответил: «Потому что он не был реализован объектно-ориентированным способом». В этот момент он сказал мне: «Почему бы и нет, все это было реализовано внутри класса, поэтому оно было объектно-ориентированным. ». Хорошо, с технической точки зрения, он был реализован внутри класса, поэтому он был объектно-ориентированным, с другой стороны, он не следовал ничему из того, что означает для меня объектно-ориентированное программирование.
В этой статье я хотел бы показать вам, что объектно-ориентированное программирование - это не просто использование связанных элементов языка программирования, это нечто гораздо большее. На мой взгляд, вы можете также выполнять объектно-ориентированное программирование на языках, которые официально не поддерживают его (например, C), и вы также можете работать не объектно-ориентированным способом на чистых языках ООП (таких как Java или C #). Позвольте мне показать вам мое понимание ООП.
Принципы объектно-ориентированного программирования
Прежде всего, давайте посмотрим на официальное определение ООП. Я нашел четкое определение здесь.
В источнике говорится:
«Объектно-ориентированное программирование (ООП) - это модель программирования, построенная вокруг объектов. Эта модель разделяет данные на объекты (поля данных) и описывает содержимое и поведение объекта посредством объявления классов (методов).
Возможности ООП включают в себя следующее:
- Инкапсуляция. Облегчает управление структурой программы, поскольку реализация и состояние каждого объекта скрыты за четко определенными границами.
- Полиморфизм: означает, что абстрактные объекты реализуются несколькими способами.
- Наследование: относится к иерархическому расположению фрагментов реализации.
Объектно-ориентированное программирование позволяет упростить программирование. Его преимущества включают в себя повторное использование, рефакторинг, расширяемость, обслуживание и эффективность».
Давайте сперва разберемся с возможностями:
- Инкапсуляция: я думаю, что это самая важная возможность. Каждый из объектов имеет состояние, которое скрыто для других объектов. И с другой стороны, есть «реализация» для каждого объекта. Под этим я понимаю функции-члены объектов. И состояние, и реализация вместе составляют единое целое или, по терминологии ООП, один объект. Поэтому, если ваш объект является прямоугольником, он будет иметь состояние с такими элементами, как ширина, высота, позиция, цвет и т. д. и у него будет реализация, реализованные функции-члены, такие как: перемещение, изменение размера, рисование и т. д. И это один объект, не меньше, не больше.
- Наследование: я бы упомянул об этом до полиморфизма, потому что иначе это трудно понять. Наследование означает связь между родительскими и дочерними классами, иначе говоря, базовыми и унаследованными классами. Таким образом, для каждого класса вы можете определить так называемые дочерние классы, которые могут делать все, что может делать базовый класс (имеет те же публичные методы и переменные-члены), но это может переопределить реализацию методов и может расширить функциональность (ввести новые функции-члены и переменные-члены). Например, у вас есть класс для животных. Каждое животное может двигаться само. Но как они могут двигаться сами, если методы различны для каждого животного. Таким образом, различные животные будут унаследованы от базового класса Animal и переопределят реализацию метода move. Плюс у некоторых животных есть некоторые дополнительные навыки, например, лай собаки. Таким образом, вы можете определить метод лая для класса Dog.
- Полиморфизм: это именно то, что я рассказал о методе перемещения животных. Например, у вас может быть коллекция животных, и вы можете вызывать метод перемещения для каждого из них, и вы не должны беспокоиться о том, как он реализован для животного.
Это официальная теория. Давайте пойдем дальше с практической теорией.
Объекты, классы и связи между ними
Из названия объектно-ориентированного программирования мы видим, что объекты являются основными единицами этой парадигмы. Следующей важной особенностью является класс. Но какова связь между объектами и классами: класс на самом деле является коллекцией, шаблоном для похожих объектов, для объектов с такими же методами и переменными-членами. Так что если у вас есть прямоугольник, то у вас будет объект прямоугольника, который является экземпляром класса Rectangle. Класс прямоугольника определяет, что он должен иметь размер и позицию, а объект содержит свой точный размер и позицию. Существуют языки программирования (например, JavaScript), в которых нет необходимости определять класс объекта. В этом случае объекты создаются на основе так называемых прототипов.
Поэтому, когда вы планируете объектно-ориентированную программу, сначала вам всегда нужно подумать о том, какие классы вам понадобятся в вашей программе.
Другой важный вопрос: каковы связи между этими классами?
Существует 4 типа связей между классами:
Композиция
Это означает, что связанный класс принадлежит нашему классу. Они имеют одинаковое время жизни (они создаются и удаляются одновременно). Наш класс не может работать без подключенного класса. Например, если у нас есть класс House и класс Wall, то, скорее всего, между ними будет композиционная связь.
Агрегация
Агрегация между классами A и B означает, что у класса A есть экземпляр класса B. Как у машины есть владелец. Но владелец может существовать и без машины, а машина может существовать без владельца - владелец может измениться в любое время.
Ассоциация
Ассоциация между классом A и B означает, что класс A использует экземпляр класса B. Обычно это отношение между объектами является просто краткосрочным отношением, только во время выполнения определенной функции. Например, у вас есть класс, и у него есть функция Log. Но этой функции Log нужен экземпляр класса Logger в качестве параметра. Это типичный пример для ассоциации.
Наследование / Обобщение
Это тип отношения, уже упомянутый ранее. Это означает, что класс A является частным случаем класса B. Он также называется дочерним / родительским классом.
Следующее, что нужно сделать, это решить, какие отношения существуют между вашими классами. Хорошо бы сделать графическое моделирование для ваших классов, чтобы иметь лучший обзор. Хорошим инструментом для этого служит диаграмма классов UML. Я бы предложил всегда составлять план ваших классов хотя бы на бумаге перед началом программирования.
Есть два дополнительных определения, которые я хотел уточнить:
Состояние объекта: На самом деле, ого построено на основе текущего значения всех переменных-членов. Поэтому, если вы изменяете хотя бы одну переменную-член, состояние вашего объекта также меняется.
Открытый интерфейс класса: это все открытые методы и переменные, которые видны снаружи класса.
Исходя из этого, вы можете составить план необходимых классов, но в программировании ООП все еще есть некоторые так называемые «лучшие практики».
SOLID
Эти лучшие практики называются SOLID. Это означает следующее:
Принцип единоличной ответственности
Каждый из ваших классов должен иметь простую четкую ответственность. Вы должны быть в состоянии описать назначение класса одним предложением, не используя слова «и» и «или». Так что да: огромные классы - это большинство случаев против этого принципа.
Принцип открытости/закрытости
Ваш код должен быть открыт для расширений, но закрыт для изменений.
Принцип замещения Барбары Лисков
Это может быть немного сложнее. Но это означает, что каждый из ваших родительских классов может быть заменен любым из его дочерних классов без нарушения функциональности. Это происходит от того, что называется «Дизайн по контракту».
Принцип разделения интерфейса
Ваши интерфейсы должны быть маленькими, четкими и иметь одну четко определенную цель. Таким образом, вы должны избегать интерфейсов, которые возвращают много данных. Поэтому общедоступный интерфейс вашего класса должен быть хорошо продуман и прост в использовании. Делать все открытым - не очень хорошая практика.
Принцип обращения зависимостей
Вы должны проектировать свои классы таким образом, чтобы классы, которые являются зависимостями вашего класса, могли быть установлены через некоторые функции-установщики или параметры конструктора. Так что вы можете изменить их на любой подтип позже. Так, например, если у вас есть класс Logger, который используется для создания файлов журнала, и вы можете изменить его с помощью функции установщика, вы можете изменить свой Logger на XMLLogger, JSONLogger или SimpleLogger, если все эти классы являются производными от одного и того же Логгер базовый класс. Также полезно, если при модульном тестировании вам нужно смоделировать ваши зависимости.
Позвольте мне расширить его с моими собственными предложениями:
- Всегда старайтесь держать все переменные-члены (т.е. состояние вашего класса) закрытыми и используйте функции setter и getter для большего контроля.
- Всегда старайтесь создать удобный для пользователя публичный интерфейс для вашего класса.
- Избегайте длинных списков параметров в ваших функциях-членах, в этом случае всегда думайте о преобразовании некоторых параметров в переменные-члены, если это действительно имеет смысл.
- Читайте об объектно-ориентированных шаблонах проектирования и старайтесь использовать их всегда, когда это возможно.
Объектно-ориентированное программирование на практике
Теперь, после знакомства со всей теорией, я хотел бы привести короткий практический пример. Речь идет о реализации Canvas, которая может визуализировать различные формы, такие как прямоугольник, круг, треугольник и звезда.
Не совсем объектно-ориентированным решением было бы создание одного класса Canvas с методами-членами, такими как: drawRectangle, drawCircle и т. Д. Но это решение нехорошо, оно не использует возможности объектно-ориентированного программирования и затрудняет расширение вашего кода. (это против принципа открытости/закрытости).
Исходя из этого, мое предложение будет следующим:
Создайте класс Canvas. Он должен иметь коллекцию фигур (агрегирование с классом Shape). В нем также должны быть методы-члены, такие как addShape для добавления новых фигур или deleteShape для удаления уже добавленных фигур. Он должен иметь функцию drawAll, которая рисует все добавленные фигуры. Shape - это базовый класс, который имеет метод draw. Каждая фигура имеет размер (композицию) и позицию. Position - это класс с атрибутами для координат x и y. Размер имеет атрибуты ширины и высоты. Каждая конкретная фигура (прямоугольник, треугольник, круг и звезда) являются дочерними классами фигуры. Так что у каждого из них есть позиция, размер и все они должны реализовывать функцию рисования. С этим решением довольно легко расширить программу новыми формами (просто внедрить новые дочерние классы), чтобы изменить представление позиции в другой системе координат.
Это моделируется и визуализируется в диаграмме классов UML должно быть что-то похожее:
Резюме
Если вы опытный разработчик программного обеспечения, возможно, я не сказал вам ничего нового, но, исходя из своего опыта, многие программисты имеют неясное понимание этой темы, поэтому я очень надеюсь, что смогу помочь многим из них. Основываясь на моем опыте с этим пониманием, вы можете обеспечить гораздо лучшее качество своей работы.
Марсель Липп