Как «Змейка» может познакомить с ООП: сложная концепция простыми словами

Здравствуйте! Вас приветствует редакция сайта GeekBrains.ru, сервиса онлайн-обучения программированию. Мы решили завести блог на Хабре! Уверены, что ещё успеем рассказать и обсудить много интересного из мира программирования, ИТ и онлайн-образования. Но начнём очень просто, без особых прелюдий, с обзора бесплатного курса по основам C# и ООП от одного из наших учеников. Слоган курса гласит «Сложная концепция простыми словами». Давайте же посмотрим, насколько это соответствует действительности.

d071d0dd31e8469fb7f0b7f82ce7ba93.png
Пара слов о слушателе: менеджер IT-проекта, знаком с процедурным программированием, web-разработкой, SQL. Более тесное знакомство с ООП понадобилось для глубокого внедрения в бизнес-процессы. Итак, слово нашему выпускнику.

«Есть такая программистская шутка: язык программирования легко выучить — в отличие от иностранных языков, в нём совсем немного слов. Действительно, выучить названия команд, функций, библиотек, типы данных и даже синтаксис не так сложно, тем более, что многие из них схожи в разных языках. Однако недаром высказывание названо шуткой — для того, чтобы работать с конкретным языком программирования, нужно знать принципы, основы парадигмы, стандарты языка. Объектно-ориентированное программирование, как и любая парадигма программирования, имеет набор принципов и правил, которые справедливы для всех языков и пригодятся в любом случае.

Для себя я выбрал ознакомительный курс для освоения принципов объектно-ориентированного программирования, который построен на создании рабочего проекта на языке С# — консольной игры «Змейка». Это та самая змейка, за которой несколько поколений убивали время на лекциях, играя на тетрисах и на чёрно-белых телефонах Nokia. Но должен сказать, что писать свою игрушку значительно приятнее, а, главное, полезнее. В ходе создания игры преподаватель раскрывает все принципы ООП, причём так, что каждый принцип воспринимается не как навязанная скучная теория, а как решение уже назревшего в голове вопроса: «Как упростить код и сделать его читабельнее?» Но всё по порядку.

В начале программы я погрузился сразу в два современных инструмента, которые используют разработчики.

  • Visual Studio — интегрированную среду разработки для ряда языков программирования. Именно в Visual Studio можно познакомиться с редактором исходного кода, дизайнером классов, отладчиком и консолью.
  • GitHub — веб-сервис для хостинга IT-проектов и их совместной разработки, базирующийся на системе управления версиями Git. Знакомство с ним помогает понять, как устроен проект, обратиться к открытому коду, скопировать его, если это необходимо, просмотреть предшествующие версии кода. Для общения среды разработки и репозитория кода используется приложение Smartgit.


Выбранный язык — C#, но, как я уже понял из своей практики, принципы ООП одни и те же и могут быть легко применены при изучении другого языка.

Изначально преподаватель делает акцент на том, что будет разрабатываться продукт. Выбор на змейку пал неслучайно — всем известна логика игры, её особенности и требования. А в разработке важно уже на раннем этапе иметь целостное представление о будущем проекте. Такое видение помогает разбить его на значимые этапы и избежать многих упущений.

Первые два урока просты и понятны любому человеку, даже совершенно не знакомому с программированием. Традиционно работа начинается со счастливого ‘Hello, world!’

namespace Snake
{
        class Program
        {
                static void Main( string[] args )
                {
                        Console.WriteLine("Hello world");
                        Console.ReadLine();
                }
        }
}


Я ещё раз повторил для себя, что такое функция, как она работает, как создаются переменные. Для написания кода используется процедурный подход — функции последовательно применяются, принимая на входе заданные параметры. Сразу становятся очевидными два недостатка создания всего кода внутри главной функции main: разрастание кода и объявление переменных прямо внутри этой функции.

namespace Snake
{
        class Program
        {
                static void Main( string[] args )
                {
                        int x1 = 1;
                        int y1 = 3;
                        char sym1 = '*';

                        Draw( x1, y1, sym1 );

                        int x2 = 4;
                        int y2 = 5;
                        char sym2 = '#';

                        Draw( x2, y2, sym2 );

                        Console.ReadLine();
                }

                static void Draw(int x, int y, char sym)
                {
                        Console.SetCursorPosition( x, y );
                        Console.Write( sym );
                }
        }
}


Действительно, после первых двух занятий возникает чувство восторга от понимания довольно сложных вещей. Проверил на себе, поэтому подписываюсь под комментариями слушателей.

24e790f0658344109d218648b1f9810e.png

На третьей лекции я познакомился с понятием класса, типа данных. Класс — одно из основных понятий ООП, поэтому его изучению уделяется пристальное внимание. Переменные начинают создаваться как экземпляры класса, то есть объекты (отсюда и название ООП).

Если слушатель начинающий, то он учится понимать язык кода и выражение Point p1 = new Point(); начинает восприниматься как «создаётся объект точка p1 как экземпляр класса Point, принимающий на входе координаты».

namespace Snake
{
        class Point
        {
                public int x;
                public int y;
                public char sym;

                public void Draw()
                {
                        Console.SetCursorPosition( x, y );
                        Console.Write( sym );                   
                }
        }
}


На этом же занятии слушатель учится думать, как компьютер. Это происходит с помощью использования точки останова и прохода по коду через отладчик: шаг за шагом можно видеть создание объектов класса, инициализацию переменных, работу функции (вызов метода Draw).

c30de8db1a0146d29da8c0c48c703be5.png

На четвёртом занятии создаётся конструктор класса Point — явно написанный конструктор со специальным синтаксисом, который ничего не возвращает.

public Point(int _x, int _y, char _sym)
                {
                        x = _x;
                        y = _y;
                        sym = _sym;
                }


Я заметил, как сокращается объём кода основной программы после создания конструктора. Конструктор принимает на вход координаты точки и символ её обозначения, но деталей реализации пользователь не видит — они скрыты внутри конструктора. Так я столкнулся с первым из трёх принципов ООП — инкапсуляцией. Инкапсуляция — это свойство системы, позволяющее объединять данные и методы, работающие с ними, в классе и скрыть все детали реализации от пользователя.

Пятая лекция погружает в вопрос организации памяти, работы программы со стэком и кучей. Объяснения дополнены наглядными схемами. После этого начинается работа с новым классом стандартной библиотеки C# List (список), в котором создаются функции добавления и удаления элемента, а также возникает цикл foreach.

List<int> numList = new List<int>();
                        numList.Add( 0 );
                        numList.Add( 1 );
                        numList.Add( 2 );

                        int x = numList[ 0 ];
                        int y = numList[ 1 ];
                        int z = numList[ 2 ];

                        foreach(int i in numList)
                        {
                                Console.WriteLine( i );
                        }

                        numList.RemoveAt( 0 );

                        List<Point> pList = new List<Point>();
                        pList.Add( p1 );
                        pList.Add( p2 );

                        Console.ReadLine();
                }
        }


Работая с циклом в отладчике, слушатель ещё более наглядно понимает структуру и последовательность работы программы.
Для целей реализации игры мы создали горизонтальные и вертикальные линии препятствий для змейки, которые представляют собой не что иное как список точек. Я старался успевать следовать за преподавателем, разбирая его код, создавая свой и тренируясь уже в своей программе.

namespace Snake
{
        class HorizontalLine
        {
                List<Point> pList;

                public HorizontalLine(int xLeft, int xRight, int y, char sym)
                {
                        pList = new List<Point>();
                        for(int x = xLeft; x <= xRight; x++)
                        {
                                Point p = new Point( x, y, sym );
                                pList.Add( p );
                        }
                        
                }

                public void Drow()
                {
                        foreach(Point p in pList)
                        {
                                p.Draw();
                        }
                }
        }
}


Преподаватель отмечает, что и точка, и линии, а в дальнейшем и сама подвижная змейка по сути являются фигурами, поэтому должно существовать какое-то решение для оптимизации кода, которое позволит не копировать код, а переиспользовать его. Так я познакомился со вторым принципом ООП — наследованием. Наследование — это свойство системы, позволяющее описывать новый класс на основе уже существующего с частично или полностью замещающейся функциональностью. Таким образом, каждая линия, змейка и точка становятся частным случаем (наследуются) от класса Фигура: class HorizontalLine: Figure.

namespace Snake
{
        class Figure
        {
                protected List<Point> pList;

                public void Drow()
                {
                        foreach ( Point p in pList )
                        {
                                p.Draw();
                        }
                }
        }
}


Наследный класс обязательно содержит признаки родительского класса, но может иметь и свои собственные. Пример наследования дополнительно разбирается на хрестоматийном и понятном примере класса Работник, унаследованном от класса Человек, имеющим от родительского класса рост и возраст и свой признак — зарплату. Кстати, для целей самостоятельной тренировки понимания наследования в ООП лучше всего работать именно с проектированием карточки студента или сотрудника — это я понял сразу, сперва самостоятельно закрепляя свои знания, а потом уже работая с проектом.

И вот змейка должна научиться двигаться в поле и управляться стрелками с клавиатуры. Задача кажется сложной, но я помнил, что речь идёт всё ещё о консоли и поэтому реализация передвижения змейки должна быть максимально простой. Я уже знал, что змейка должна двигать в четырёх направлениях, что-то есть, расти или уменьшаться. И вот тут наступает пора абстрагирования — ситуации, при которой код пишется на основе выбранных значимых характеристик объекта, а незначительные — исключаются. Выбираем значимые признаки: змейка — это фигура из точек на карте, у неё есть стартовая позиция, координаты и она движется в одном из четырёх направлений. Класс Snake серьёзно изменяется и растёт.

{
        class Snake : Figure
        {
                public Snake( Point tail, int length, Direction direction )
                Direction direction;

                public Snake( Point tail, int length, Direction _direction )
                {
                        direction = _direction;
                        pList = new List<Point>();
                        for(int i = 0; i < length; i++)
                        for ( int i = 0; i < length; i++ )
                        {
                                Point p = new Point( tail );
                                p.Move( i, direction );
                                pList.Add( p );
                        }
                }

                internal void Move()
                {
                        Point tail = pList.First();                     
                        pList.Remove( tail );
                        Point head = GetNextPoint();
                        pList.Add( head );

                        tail.Clear();
                        head.Draw();
                }

                public Point GetNextPoint()
                {
                        Point head = pList.Last();
                        Point nextPoint = new Point( head );
                        nextPoint.Move( 1, direction );
                        return nextPoint;
                }
        }
 }


Вообще, если продолжить говорить об абстрагировании, в ООП широко используется понятие абстрактного класса. Создаётся шаблонный класс, который реализует только известную и нужную разработчику на данный момент функциональность. Классы, производные от абстрактного, всю функциональность в дальнейшем смогут дополнить.
Но вернёмся к проекту. Появляется класс Direction (направление), в котором используется ещё один тип данных enum — перечисление, состоящее из набора именованных констант. В нашем случае это константы-направления: right, left, up, down. У класса Point появляется метод Move.

public void Move(int offset, Direction direction)
                {
                        if(direction == Direction.RIGHT)
                        {
                                x = x + offset;
                        }
                        else if(direction == Direction.LEFT)
                        {
                                x = x - offset;
                        }
                        else if(direction == Direction.UP)
                        {
                                y = y + offset;
                        }
                        else if(direction == Direction.DOWN)
                        {
                                y = y - offset;
                        }
                }


Таким образом, передвижение змейки реализовано как сдвиг позиции с перетиранием хвоста пробелом. Змейка управляется клавишами и управление реализовано следующим образом.

public void HandleKey(ConsoleKey key)
                {
                        if ( key == ConsoleKey.LeftArrow )
                                direction = Direction.LEFT;
                        else if ( key == ConsoleKey.RightArrow )
                                direction = Direction.RIGHT;
                        else if ( key == ConsoleKey.DownArrow )
                                direction = Direction.DOWN;
                        else if ( key == ConsoleKey.UpArrow )
                                direction = Direction.UP;
                }


Вновь я столкнулся с инкапсуляцией — управление змейкой уходит в класс Snake.
На следующем этапе змейка начинает есть и добыча создаётся в бесконечном цикле с помощью функции FoodCreator, проверяется совпадение координат головы змейки и точки, представляющей собой еду.

while (true)
                        {
                                if(snake.Eat( food ) )
                                {
                                        food = foodCreator.CreateFood();
                                        food.Draw();
                                }
                                else
                                {
                                        snake.Move();
                                }                                       

                                Thread.Sleep( 100 );

                                if (Console.KeyAvailable)
                                {
                                        ConsoleKeyInfo key = Console.ReadKey();
                                        snake.HandleKey( key.Key );
                                }
                        }


Создавая препятствия для кушающей в бесконечном цикле змейки и работая над классом Wall, я узнал о третьей парадигме ООП — полиморфизме, способности функции обрабатывать данные разных типов. В ООП полиморфизм заключается в том, что объект использует методы производного класса, которого нет на момент создания базового. Во время выполнения объекты производного класса могут рассматриваться как объекты базового класса в таких местах, как параметры метода, коллекции или массивы. Когда это происходит, объявленный тип перестает соответствовать самому типу во время выполнения. Сразу оговорюсь, что полиморфизм понимается не сразу, мне понадобилось послушать лекцию ещё раз и обратиться к замечательному учебнику Шилдта, который давно лежал под рукой и ждал своего часа.

На последнем занятии змейка стала вполне самостоятельной, а я учился обрабатывать столкновения с препятствиями и собственным хвостом змейки. Код в лекции уже не создаётся, а берётся из репозитория и разбирается. Я не пошёл на поводу у соблазна скопировать чужой код, а некоторое время после прослушивания курса создавал свой, раз за разом обращаясь к лекциям. Вам я советую поступать точно так же — потому что для работы и понимания нужны знания. Надеюсь, я дал достаточно тизеров, чтобы вам захотелось зайти на GitHub и разобраться в реализации простой игры, основной код которой составляет всего 52 строки, а это значит, что все принципы ООП были успешно применены.

Подводя итоги, преподаватель ещё раз возвращается к главным парадигмам ООП и обращает внимание на модификаторы доступа public и private и рассказывает о ключевом слове virtual, благодаря которому метод может быть переопределён в наследном классе. Private — это закрытые данные и код внутри объекта, public — открытые. Закрытые данные и код доступны только из другой части этого же объекта, то есть извне к ним обратиться нельзя. Открытые данные и код доступны из любой части программы и нередко служат интерфейсом к закрытым частям объекта.
Если говорить о курсе в целом, то он мне помог — изменились и качество моей работы, и уровень общения с разработчиками. Советую попробовать всем, кому хоть немного интересно программирование, как минимум, это развивает мозг и учит думать системно. Я точно вернусь послушать другие курсы и пообщаться с профессионалами. Ну, а отважным новичкам желаю удачи!»

Вы заметили, насколько популярным стал видеоформат в контенте, рекламе, управлении? Общеизвестно, что видео задействует сразу и зрение, и слух, а значит, воспринимается лучше. К тому же, видеокурс можно остановить, перемотать, прослушать ещё несколько раз, задать вопросы в комментариях. Плюс ко всему, в GeekBrains преподают практики, для которых программирование — ежедневная работа и поэтому они всегда в курсе самых последних тенденций своей отрасли. Конечно, просмотр курса со стаканом чая перед монитором принесёт мало пользы, поэтому в заключение хотим дать несколько советов слушателям.

  • Слушайте курс с карандашом или ручкой — записывайте моменты, которые стоит переслушать или дополнительно посмотреть в Интернете или книге.
  • Не пропускайте непонятные моменты, старайтесь разобраться, обращайтесь к дополнительным источникам.
  • Не копируйте код, создавайте свой — только это позволит вам по-настоящему научиться работать с кодом.
  • Работайте с отладчиком — знание того, как пошагово работает программа, помогает создавать стройные и понятные функции.
  • Возвращайтесь к теории и курсу даже когда вы уже смогли создать своё приложение — это помогает упорядочить знания и найти новую точку для развития.


Мы уверены, что время на самообразование никогда не бывает потраченным зря. Оно обязательно окупится.

© Habrahabr.ru