Классы
Классы
За всю недолгую историю вычислительной техники никто не написал идеального программного обеспечения. Маловероятно, что первым станете вы. — Энди Хант, автор книги «Программист‐прагматик»
Согласно книге Роберта К. Мартина «Чистый код», первое правило классов гласит: классы должны быть небольшими. Второе правило: они должны быть еще меньше. Ограничение размера класса делает его более целенаправленным и связным. Существующий класс легко обрастает функциями, пока не становится перегруженным. Осознанно делайте классы небольшими: крупные раздутые классы трудно читать и отлаживать.
Метафора газетной статьи Представьте исходный код класса как газетную статью. Чтение начинается сверху: сначала внимание привлекают заголовок и имя автора. Вводный абзац дает общее представление, а по мере продвижения вниз раскрываются подробности.
Журналисты называют такую структуру перевернутой пирамидой. Самое важное излагается в начале, а тонкости раскрываются ближе к концу статьи. Класс должен следовать тому же принципу. Располагайте код сверху вниз и выстраивайте методы в иерархию. Методы верхнего уровня задают общую картину — размещайте их первыми. Ниже располагайте низкоуровневые методы с деталями реализации. Например, метод ThrowBall может вызывать SetInitialVelocity и CalculateTrajectory. Сначала разместите ThrowBall, поскольку он описывает основное действие, а затем добавьте вспомогательные методы. Каждая газетная статья коротка, но множество статей вместе образуют единое целое — газету или новостной сайт. Так же устроен проект Unity: многочисленные классы должны совместно образовывать крупное, но связное приложение.
Организация класса Каждому классу нужна единая структура. Для порядка группируйте члены класса по разделам: — Поля — Свойства — События и делегаты — Методы MonoBehaviour (Awake, Start, OnEnable, OnDisable, OnDestroy и т. д.) — Открытые методы — Закрытые методы Помните правило именования классов в Unity: имя исходного файла должно совпадать с именем содержащегося в нем MonoBehaviour. В файле могут находиться и другие внутренние классы, однако MonoBehaviour в каждом файле должен быть только один, если только несколько MonoBehaviour не разделены пространствами имен.
Принцип единственной ответственности Помните: каждый класс должен оставаться небольшим. В проектировании ПО принцип единственной ответственности помогает сохранять простоту. Суть принципа в том, что каждый модуль, класс или функция отвечает только за одну задачу. Допустим, вы создаете игру Pong. Начать можно с классов ракетки, мяча и стены.
Сыграем в Pong?
Например, классу Paddle может потребоваться: — хранить основные данные о скорости движения — считывать ввод с клавиатуры — перемещать ракетку в ответ на ввод — воспроизводить звук при столкновении с мячом
Поскольку игра устроена просто, все перечисленные задачи можно объединить в базовом классе Paddle. Технически один MonoBehaviour способен выполнить все необходимое.
Хранить данные о скорости Считывать ввод с клавиатуры Перемещать ракетку Воспроизводить звук
Один MonoBehaviour выполняет все задачи
Однако объединение всего в одном классе, даже небольшом, усложняет архитектуру из-за смешения обязанностей. Данные переплетаются с вводом, а класс должен обрабатывать и то и другое. Вопреки принципу KISS несколько простых задач оказываются тесно связанными. Вместо этого разбейте Paddle на небольшие классы с одной ответственностью у каждого. Вынесите данные в PaddleData или ScriptableObject, а остальную логику распределите между PaddleInput, PaddleMovement и PaddleAudio. Класс PaddleLogic может обрабатывать ввод из PaddleInput. Используя данные о скорости из PaddleData, он перемещает ракетку через PaddleMovement. Наконец, при столкновении мяча с ракеткой PaddleLogic сообщает PaddleAudio, что нужно воспроизвести звук.
Скорость
Клавиатура
Движение
Звуки
Разделение обязанностей при рефакторинге класса Paddle
В новой архитектуре каждый класс выполняет одну задачу и остается небольшим и понятным. Чтобы проследить логику, больше не нужно прокручивать несколько экранов кода.
Скрипт Paddle по-прежнему нужен, но теперь он только связывает остальные классы. Основная функциональность распределена между ними. Чистый код не всегда самый компактный. Даже при использовании небольших классов после рефакторинга общее число строк может вырасти, зато каждый класс читается проще. При отладке и добавлении функций такая структура помогает сохранять порядок.
Пример рефакторинга Подробный разбор рефакторинга небольшого проекта приведен в статье «Как проектировать код по мере роста проекта». В ней показано, как разделить крупные MonoBehaviour на небольшие компоненты по принципу единственной ответственности. Также можно посмотреть оригинальный доклад Микаэля Калмса «От Pong до проекта с командой из 15 человек», представленный на Unite Berlin.
Classes
No one in the brief history of computing has ever written a piece of perfect software. It’s unlikely that you’ll be the first. – Andy Hunt, author of The Pragmatic Programmer
According to Robert C. Martin’s Clean Code, the first rule of classes is that they should be small. The second rule is they should be even smaller than that. Limiting the size of each class makes it more focused and cohesive. It’s easy to keep adding on top of an existing class until it overextends with functionality. Instead make a conscious effort to keep the classes short. Big, bloated classes become difficult to read and troubleshoot.
The newspaper metaphor Imagine the source code of a class as a news article. You start reading from the top, where the headline and byline catch your eye. The lead-in paragraph gives you a rough summary. Then you glean more details as you continue downward.
Journalists call this the inverted pyramid. The broad strokes of most newsworthy items appear at the beginning. You only get the story’s nuances as you read to the end. Your class should also follow this basic pattern. Organize top-down and think of your functions as forming a hierarchy. Some methods serve a higher-level and lay the groundwork for the big picture. Put these first. Then, place lower-level functions with implementation details later. For example, you might make a method called ThrowBall that references other methods, SetInitialVelocity and CalculateTrajectory. Keep ThrowBall first, since that describes the main action. Then, add the supporting methods below it. Though each news article is short, a newspaper or news website will have many such collected stories. When taken together, the articles comprise a unified, functional whole. Think of your Unity project in the same way. It has numerous classes that must come together to form a larger, yet coherent, application.
Class organization Each class will need some standardization. Group class members into sections to organize them: —
Fields
Properties
Events / Delegates
Monobehaviour Methods (Awake, Start, OnEnable, OnDisable, OnDestroy, etc.)
Public Methods
Private Methods
Recall the recommended class naming rules in Unity: the source file name must match the name of the Monobehaviour in the file. You might have other internal classes in the file, but only one Monobehaviour should exist per file unless separated by namespaces.
Single-responsibility principle Remember the goal is to keep each class short. In software design, the single-responsibility principle guides you toward simplicity. The idea is that each module, class, or function is responsible for one thing. Suppose you want to build a game of Pong. You might start with classes for a paddle, a ball, and a wall.
Fancy a game of Pong?
For example, a Paddle class might need to: —
Store basic data about how fast it can move
Check keyboard input
Move the paddle in response
Play a sound when colliding with a ball
Because the game design is simple, you can incorporate all of these things into a basic Paddle class. In fact, it’s entirely possible to create one Monobehaviour that does everything you need.
One Monobehaviour doing everything
However, keeping everything as part of one class, even a small one, complicates the design by mixing responsibilities. The data intertwines with the input, while the class needs to apply logic to both. Contrary to the KISS principle, you’ve taken a few simple things and entangled them. Instead, break your Paddle class into smaller classes, each with a single responsibility. Separate data into its own PaddleData class or use a ScriptableObject. Then refactor everything else into a PaddleInput class, a PaddleMovement class, and a PaddleAudio class. A PaddleLogic class can process the input from the PaddleInput. Applying the speed information from the PaddleData, it can shift the paddle using the PaddleMovement. Finally, the PaddleLogic can notify the PaddleAudio to play a sound when the ball collides with the paddle.
Refactor a Paddle class into single responsibilities
Each class does one thing in this redesign and fits into small, digestible pieces. You don’t need to scroll through several screens to follow the code. You’ll still require a Paddle script but its sole job is to tie these other classes together. The bulk of the functionality is split into the other classes. Note that clean code is not always the most compact code. Even when you use shorter classes, the total number of lines may increase during refactoring. However, each individual class becomes easier to read. When the time comes to debug or add new features, this simplified structure helps keep everything in its place.
Refactoring example For a more in-depth look at refactoring a simple project, see How to architect code as your project scales. This article demonstrates how to break down larger Monobehaviours into smaller pieces using the single-responsibility principle. You can also watch Mikael Kalms’ original presentation, “From Pong to 15-person project,” at Unite Berlin.