Переписать код спиральной матрицы из процедурного стиля в ООП
Суть вопроса: знаю как решать задачи на уровне процедурного стиля, но уже давно пора забыть это дело и переключаться на ООП. На курсах по c# как раз начинаем проходить всякие классы, конструкторы и т.д. Вот мой код в процедурно-методном стиле, что ли. Как бы вы его написали в чистейшем ООП? Мне нужно для сравнения, что б я на что-то равнялся. Понятно, что еще не знаю всякие лямбды, да и Linq не весь, но мне нужно. (Задача по выводу спиральной матрицы)
public static int[,] GetMatrix(int size)
{
int[,] myArray = new int[size, size];
for (var i = 0; i < size; ++i)
{
for (var j = 0; j < size; ++j)
{
if (i > j)
{
WriteNumber(size, myArray, i, j);
}
else
{
WriteNumberTwo(size, myArray, i, j);
}
}
}
return myArray;
}
private static void WriteNumberTwo(int size, int[,] myArray, int i, int j)
{
if (i + j < size)
{
myArray[i, j] = i * (size - i) * 4 - (i - (size - i)) + i + j - size + 1;
}
else
{
myArray[i, j] = (size - (size - j - 1)) * (size - j - 1) * 4 + (j - (size - j)) + i + (j - size) + 3;
}
}
private static void WriteNumber(int size, int[,] myArray, int i, int j)
{
if (i + j < size)
{
myArray[i, j] = (size - (j + 1)) * (j + 1) * 4 - (i - (j + 1));
}
else
{
myArray[i, j] = i * (size - i) * 4 - (i - (size - i)) - (j - size + i + 1);
}
}
Ответы (2 шт):
Самый очевидный и простой вариант, с которого следует начать - это упаковать в класс SpiralMatrix чтобы приватные методы не торчали наружу:
public class SpiralMatrix
{
public static int[,] Create(int size)
{
var result = new int[size, size];
for (var i = 0; i < size; ++i)
{
for (var j = 0; j < size; ++j)
{
if (i > j)
{
WriteNumber(size, result, i, j);
}
else
{
WriteNumberTwo(size, result, i, j);
}
}
}
return result;
}
private static void WriteNumberTwo(int size, int[,] myArray, int i, int j)
{
if (i + j < size)
{
myArray[i, j] = i * (size - i) * 4 - (i - (size - i)) + i + j - size + 1;
}
else
{
myArray[i, j] = (size - (size - j - 1)) * (size - j - 1) * 4 + (j - (size - j)) + i + (j - size) + 3;
}
}
private static void WriteNumber(int size, int[,] myArray, int i, int j)
{
if (i + j < size)
{
myArray[i, j] = (size - (j + 1)) * (j + 1) * 4 - (i - (j + 1));
}
else
{
myArray[i, j] = i * (size - i) * 4 - (i - (size - i)) - (j - size + i + 1);
}
}
}
Ну и как бы всё, уже считай пишете в ООП-стиле:
var size = 3;
var spiralMatrix = new SpiralMatrix().Create(size);
(В реальном проекте код этого класса будет в отдельном файле и вы можете выкинуть из головы подробности, как именно идёт заполнение и просто писать имя класса метод, не вникая в детали реализации.
Вы так ежедневно используете классы типа "строка" или "массив", у которых дергаете разные методы, хотя в любой момент времени можете открыть исходники и посмотреть, эти классы написаны на том же самом C#, на котором вы пишете.)
Но в этой задаче особо классы и объекты не нужны, что видно хотя бы по тому, что у вас все методы статические, поэтому вы можете тоже в классе сделать все методы статическими и вызывать без инстацирования класса:
var size = 3;
var spiralMatrix = SpiralMatrix.Create(size);
Так что с одной стороны в этой задаче классы уже есть, но особых преимуществ они не дают.
Пример, где появляется классовое мышление - когда у вас есть базовый класс матрицы, есть наследники типа квадратная/прямоугольная/спиральная матрица, есть методы которые в базовом классе объявлены (транспонирование), есть какое-то переиспользование кода и т.п.
И вот на таких примерах уже более понятно становится, что в таком стиле есть свои выгоды написания.
Видите ли, программирование вам на курсе читают блоками. В первом крупном блоке вам объясняли как устроены методы и как написать тот или иной алгоритм (крупный метод разделить на мелкие, до тех пор пока каждый шаг алгоритма получится выразить в элементарных конструкциях языка программирования). Во втором крупном блоке вам объясняют классы, это как бы отдельная, паралельная тема.
И вы решили потянуть примеры из первой темы во вторую, а на них не особо наглядно видно преимущества ООП, хотя внутри класса вы будете использовать методы.
Лучше изучайте классы на примерах, которые специально подобраны в блоке про классы. Умеете крупный метод разделить на более мелкие -- хорошо, освоили процедурный стиль. Какие методы как распихать по классам, как сгруппировать -- это уже объектный стиль.
но уже давно пора забыть это дело и переключаться на ООП.
Вот да. Пара слов о когнитивщине.
как переключить мозг из процедурного стиля на ООП?
Я поменял заголовок, потому что в таком виде ответа нет и вопрос слишком оффтопичен, но попробую ответить. У вас сейчас стадия изучения ООП, когда мозг ещё не освоил мышление в этом стиле. В голове не укладывается, как это использовать. Это пройдёт. В детстве дети не умеют в абстрактное мышление. Они не понимают, что можно складывать не только три яблока плюс два яблока - но итог будет тот же, если складывать три груши и две груши. Это реально сложный интеллектуальный барьер, его проходят все и все забывают. Сейчас у вас иной барьер. Вам говорят про класс уточка и метод крякает -- и вы не понимаете, что это тоже самое, когда вы будете использовать класс "база" и класс "соединение" и "строка подключения". И когда вы придёте на реальное предприятие, скажем в банк - вы придумаете класс "банковский счёт" и класс "проводка". Но сейчас вы не понимаете и не умеете и вам дать задачу на банк - вы не увидите, что это те же самые уточки, просто вид сбоку. А вам больно думать.
Хотите быстрее пройти фазу изучения? Тренируйтесь больше!
И тренируйтесь не в написании алгоритмов, а именно в выделении объектов предметной области. Это пригодится в освоении domain driven design.
Вот например, есть игра и её предметная область разобрана именно с точки зрения классов. https://habr.com/en/post/322258/
Разберите этот пример, а потом начните придумывать свои. Десятками, сотнями. И разбирайте, думайте.
Я вас обрадую: этот барьер вполне преодолим, в нём нет ничего непосильного для человека. Вам нужна мотивация (хочу стать программистом) и вам нужна практика, тренировка (условные 10 тысяч часов).
И если вы ещё не догадались, это составная часть профессии программиста -- постоянное обучение и постоянная попытка перепрыгнуть через очередной барьер когнитивной сложности. Тут об этом подробнее написано, не все тезисы я разделяю, но в целом есть здравое зерно: https://habr.com/en/company/domclick/blog/569062/
способность длительно, в повседневном режиме выдерживать фрустрацию, возникающую от преодоления когнитивной сложности.
Детям в этом плане проще: один раз поняли арифметику -- и можно всю жизнь за прилавком проработать. Программистам в работе чаще приходится брать новые высоты.
ООП должно упрощать жизнь, а не усложнять. Распиливание кода на классы как в предыдущем ответе от @AK, который кстати содержит хорошую теоретическую часть - это распиливание ради распиливания. Возмите его ответ на вооружение как индикатор того, что вы наверное пошли не в те края, и надо пересмотреть подход к решению задачи.
Программирование с использованием ООП похоже на моделирование поведения какого-то живого организма, то есть сущности, которая что-то имеет (свойства) и умеет (методы). Объединение в одной сущности методов и свойств сущности, то есть объекта называтся инкапсуляция - первый принцип ООП.
Давайте придумаем такой организм, который решит задачу или существенно упростит ее решение. Для этого нужно ответить на 2 вопроса: что у нас имеется и что надо делать.
- Имеется матрица
- Надо ее заполнять числами двигаясь по определенному маршруту
Окей, если надо двигаться по матрице, значит есть еще 2 сущности: позиция и направление движения. Похоже на программирование какой-то игры, правда?
Давайте игрока, который будет рисовать числа на матрице назовем "Курсор". Так и назовем класс - Cursor.
Сначала свойства, потом методы.
Свойтва: ссылка на матрицу, позиция, направление.
Позиция и направление - это не стандартные типы, давайте их создадим. Для позиции я буду использовать структуру, а для направления - перечисление.
public struct Position
{
public int Row;
public int Col;
public Position(int row, int col)
{
Row = row;
Col = col;
}
}
public enum Direction
{
Right,
Down,
Left,
Up
}
Теперь можно заюзать это все в курсоре.
public class Cursor
{
private Position _position; // new Position(0, 0)
private Direction _direction; // Direction.Right
private int[,] _matrix;
public Cursor(int[,] matrix)
{
_matrix = matrix;
}
}
И вот уже курсор всё имеет, но ничего не умеет. Обратите внимание: я не присваиваю _position и _direction - это из-за того что я знаю стандартные их значения. Для первой структуры - это (0, 0) так как для int значение по умолчанию 0, а для перечисления - первое его значение Right. Не буду углубляться, но вы поизучайте эту тему с ссылочными и значимыми типами и значениями по умолчанию для типов. Скажу только если Position была бы классом - эта фишка бы не прокатила, так как для класса значение по умолчанию null, а это мне неудобно, поэтому и структура.
Что ж, теперь методы. Сначала покажу код, потом расскажу.
public void SetNumber(int number)
{
_matrix[_position.Row, _position.Col] = number;
}
public bool TryMove()
{
Position newPosition = NextPosition();
bool canMove = CheckBounds(newPosition);
if (canMove)
_position = newPosition;
return canMove;
}
public void SwitchDirection()
{
_direction = (Direction)(((int)_direction + 1) % 4);
}
private Position NextPosition()
{
switch (_direction)
{
case Direction.Right: return new Position(_position.Row, _position.Col + 1);
case Direction.Down: return new Position(_position.Row + 1, _position.Col);
case Direction.Left: return new Position(_position.Row, _position.Col - 1);
case Direction.Up: return new Position(_position.Row - 1, _position.Col);
default: throw new Exception("Непонятное направление курсора");
}
}
private bool CheckBounds(Position position)
{
return position.Row >= 0
&& position.Row < _matrix.GetLength(0)
&& position.Col >= 0
&& position.Col < _matrix.GetLength(1)
&& _matrix[position.Row, position.Col] == 0;
}
Всего 3 публичных метода. Приватные считайте я сделал для удобства и упрощения кода.
void SetNumber- записать число в матрицу по текущим координатам.bool TryMove- попытаться переместить курсор в текущем направлении, если получилось - вернутьtrue, если нет -false.SwitchDirection- хитро реализованный метод переключения направления на следующее по кругу.право-низ-лево-верх-право-низ...
Если бы я написал этот метод не зная свойств и поведения перечисления, то он выглядел бы так:
public void SwitchDirection()
{
switch (_direction)
{
case Direction.Right: _direction = Direction.Down; break;
case Direction.Down: _direction = Direction.Left; break;
case Direction.Left: _direction = Direction.Up; break;
case Direction.Up: _direction = Direction.Right; break;
default: throw new Exception("Непонятное направление курсора");
}
}
По поведению в рамках дозволенных значений перечисления оба варианта метода эквивалентны.
И вот теперь у нас есть "движок" для решения задачи. Давайте его просто используем.
static void Main(string[] args)
{
int size = 5;
int[,] matrix = new int[size, size];
Cursor cursor = new Cursor(matrix);
for (int i = 0; i < matrix.Length; i++)
{
cursor.SetNumber(i + 1);
while (i < matrix.Length - 1 && !cursor.TryMove())
cursor.SwitchDirection();
}
ShowArray(matrix);
Console.ReadKey();
}
private static void ShowArray(int[,] array)
{
for (int i = 0; i < array.GetLength(0); i++)
{
for (int j = 0; j < array.GetLength(1); j++)
{
Console.Write(array[i, j].ToString("00") + " ");
}
Console.WriteLine();
}
}
Если вы правильно скопировали код из этого ответа, то вывод в консоль будет такой:
01 02 03 04 05
16 17 18 19 06
15 24 25 20 07
14 23 22 21 08
13 12 11 10 09
Задача решена с использованием ООП. Обратите внимание на то, сколько у вас математики в процедурном подходе, и сколько у меня. ООП помогает делать решения проще и понятнее. За этим и нужно ООП.
Выше представленное решение я не считаю идеальным, но считаю хорошим примером, того, чем ООП подход к решению задач отличается от процедурного. Можно развить решение, выделив 2 класса - "курсор" и "змейка" курсор будет иметь свойства, а змейка будет заполнять матрицу. Грубо говоря, часть решения из метода Main перенесется в класс "змейка". И весь код метода Main будет состоять из 2 строчек Snake snake = new Snake(matrix) и snake.Fill(). Попробуйте сами доработать, при этом часть методов "курсора" может перекочевать в "змейку", упростив еще код в целом. Почему "змейка" - да потому что это мне очень напоминает всем известную игру, которую кстати без ООП подхода было бы просто адски сложно написать.