Переписать код спиральной матрицы из процедурного стиля в ООП

Суть вопроса: знаю как решать задачи на уровне процедурного стиля, но уже давно пора забыть это дело и переключаться на ООП. На курсах по 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 шт):

Автор решения: A K

Самый очевидный и простой вариант, с которого следует начать - это упаковать в класс 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/

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

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

→ Ссылка
Автор решения: aepot

ООП должно упрощать жизнь, а не усложнять. Распиливание кода на классы как в предыдущем ответе от @AK, который кстати содержит хорошую теоретическую часть - это распиливание ради распиливания. Возмите его ответ на вооружение как индикатор того, что вы наверное пошли не в те края, и надо пересмотреть подход к решению задачи.

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

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

  1. Имеется матрица
  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(). Попробуйте сами доработать, при этом часть методов "курсора" может перекочевать в "змейку", упростив еще код в целом. Почему "змейка" - да потому что это мне очень напоминает всем известную игру, которую кстати без ООП подхода было бы просто адски сложно написать.

→ Ссылка