2

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

A K
  • 28,718
Peterblr
  • 141
  • 10
  • так я написал (задача по выводу спиральной матрицы) – Peterblr Aug 29 '21 at 08:34
  • 1
    Функция это тот же метод только ни с чем не объеденен. Пишите как писали функции только пакуйте в класс все функции одного направления. Все. – Manul74 Aug 29 '21 at 08:34
  • 2
    Никак бы не написал. Где тут объект? – Alexey Ten Aug 29 '21 at 08:42
  • 2
    У вас в коде трижды слово static фигурирует - значит, объект не нужен. Технически, этот код можно написать в ООП-стиле, но поскольку вы хотите именно переключить мозги на ООП-стиль, то вы не поймёте, а вообще, нафига было этот код писать именно в таком стиле и что это даёт. Вам показывают классы "вот это уточка, она умеет крякать", вам показывают классы "вот это класс строка (матрица, коннекшстринг, она умеет метод метод (подставить нужный)" и вот это разбиение на объект и его методы - и есть суть подхода. – A K Aug 29 '21 at 08:44
  • 1
    Напишите класс SpiralMatrix с одним публичным методом, остальное спрячьте в приватные. Вот вам и ООП-стиль. – A K Aug 29 '21 at 08:49
  • Alexey Ten, т.е. если в курилке зайдет речь про мой код, то я смело могу говорить - он так же и в ооп стиле, правильно? – Peterblr Aug 29 '21 at 09:00
  • 1
    Разумеется нельзя. Разумеется нельзя. Упакуйте в класс (пример в ответе), вот тогда уже можете говорить. – A K Aug 29 '21 at 09:03
  • 1
    @Peterblr может это и не по ООП'шному, но быстро и "в лоб", иначе кажется не пишут (имхо). Ну это нужно уточнять у тех, кто с матрицами много работает. – Blackmeser Aug 30 '21 at 02:18

2 Answers2

3

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

aepot
  • 49,560
  • 1
    Класс! Как раз то, что нужно!!! Это и есть мой ориентир! И вопросов просто тьма! И про знак нижнего подчеркивания в названии свойств, и про структуры и куча всего. Со всем этим только предстоит познакомиться, но уже жутко интересно! Спасибо огромное за ответ! Я слышал, что более-менее писать в таком стиле, то нужно не менее 400-х часов кода написать... – Peterblr Aug 29 '21 at 13:09
  • @Peterblr знак подчеркивания перед названием приватного поля (не путайте со свойствами) позволяет с ходу визуально определить, что это - локальная переменная или поле класса не прокручивая колесом мыши код и без наведения курсора. В общем, просто политика именования - для удобства. А публичные свойства я пишу с большой буквы и без подчеркивания. Для простоты примера, в ответе я вообще не использовал свойства. – aepot Aug 29 '21 at 13:15
  • @Peterblr например в структуре public int Row { get; } было бы. – aepot Aug 29 '21 at 13:18
2

Самый очевидный и простой вариант, с которого следует начать - это упаковать в класс SpiralMatrix чтобы приватные методы не торчали наружу:

public class SpiralMatrix
{
    public static int[,] Create(int size)
    {
        var result = new int[size, size];
    for (var i = 0; i &lt; size; ++i)
    {
        for (var j = 0; j &lt; size; ++j)
        {
            if (i &gt; 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 &lt; 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 &lt; 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/

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

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

A K
  • 28,718
  • 2
    SpiralMatrix - тот случай, когда ООП без единого экземпляра написанного класса сделано. Чистая семантика, то есть ООП без ООП по сути. Не хорошо, не плохо, просто вот так. Наверное смогу показать хорошее ООП для этой задачи :) Скажем так, ваш подход "наивный", а не ООПшный, притягивание за уши. Но ответ хороший, плюс поставил. – aepot Aug 29 '21 at 10:08
  • что-то какой-то поток сознания никак не относящийся к вопросу ¯\(ツ) – Grundy Aug 29 '21 at 10:33
  • спасибо большое за разъяснения – Peterblr Aug 29 '21 at 15:25