Как работать с классами
- Задача
- Массовые и индивидуальные задачи
- Комплексная массовая задача
- Создаем класс для решателя индивидуальных задач
- Как тестировать
- Итоговый код
С ростом сложности программы, возникает необходимость в построении более продуманной архитектуры приложения.
Для написания тестов мы уже столкнулись с необходимостью разбить код на части ответственные за общение с пользователем и за часть ответственную непосредственно за логику программу.
На практике часто встречаются задачи, которые для одних и тех же данных требуются рассчитать сразу несколько результатов.
Для таких задач оказывается удобно привязывать данные непосредственно к обработчику.
Задача
Дан вектор чисел . Рассчитать
- значение суммы этих чисел
- значение произведения этих чисел
- рассчитать среднее арифметическое этих чисел
Данную задачу можно было решить так:
Выполним декомпозицию, чтобы отделить логику от общения с пользователем. У нас три результат, следовательно, потребуется три функции.
Выполнив декомпозицию, мы сделали нашу программу тестируемой. То есть для неё можно писать тесты, что есть хорошо.
Но пока код класса Logic, по сути, представляет собой просто набор функции, которые в силу особенностей языка C# нельзя хранить отдельно от класса (то есть каждая функция должна быть в каком-нибудь классе).
Такое архитектура программы в целом соответствует принципам структурного программирования. В том же языке C или C++, такой код выглядел бы попроще, никаких классов, никаких static-ов – только функции.
Но так как мы используем C#, которые дают нам использовать всю мощь объектно-ориентированного программирования, мы проползем по лестнице абстракции чуть выше.
Массовые и индивидуальные задачи
Немного формальной математики.
И так. В теории алгоритмов есть такое понятие как “Массовая задача” и “Индивидуальная задача”. Например,
-
Решить квадратное уравнение , это пример “Массовой задачи”.
-
А решить эту же задачу, при a = 1, b = 2, c = 1 это уже пример “Индивидуальной задачи”.
То есть в случае “Массовой задачи”, в задачи присутствует некоторая доля неопределенности, которая не позволяет получить ответ в виде набора констант (строковых, или числовых, или еще каких-то). То есть все знают формулы расчета корней квадратного уравнения, но как известно в зависимости от коэффициентов, полученные значения корней могут принимать какие угодно значения.
В “Индивидуальной задаче” неопределенность отсутствует, и ответ может быть получен строго определенно в виде набора констант.
Еще несколько примеров массовых и индивидуальных задач.
- Массовая задача: “Сложить числа a + b”, Индивидуальная задача: “сложить 2 + 2”
- Массовая задача: Подсчитать сумму элементов вектора . Индивидуальная задача: Подсчитать сумму элементов вектора
Как можно наблюдать всякая Массовая задача содержит в себе [бес]конечное количество индивидуальных задач.
Всякую задачу из лабораторной можно и нужно рассматривать как “Массовую задачу”. А тесты, которые мы для нее описываем можно рассматривать как некоторый набор индивидуальных задач.
Комплексная массовая задача
Вспомним как у нас выглядела исходная задача:
Дан вектор чисел . Рассчитать
- значение суммы этих чисел
- значение произведения этих чисел
- рассчитать среднее арифметическое этих чисел
Данную задачу можно рассматривать как комплексную массовую задачу.
То есть неопределенность у нас запрятана в фразе “Дан вектор чисел ”. А необходимые для расчета значения можно рассматривать как набор массовых подзадач. Эти значения можно рассчитать по следующим формулам:
– значение суммы
– значение произведения
– среднее арифметическое
Всякой “комплексной массовой задаче” можно сопоставить “комплексную индивидуальную задачу”.
То есть вариант когда дан вектор , для которого надо рассчитать сумму, произведение и среднее значение есть пример “комплексной индивидуальной задаче”.
К чему я все это? А к тому чтобы было бы удобно если бы мы могли объявить в коде программы класс в C#, которые являл бы собой олицетворение индивидуальной задачи. То есть содержал не только логику для расчета значений, но и сам набор значений, для которых он вызывается.
Попытка построить такую сущность есть пример использования объектно-ориентированного подхода в написании приложений. В общем, идем дальше.
Создаем класс для решателя индивидуальных задач
Вспомним код, который у нас был, а точнее, код класса Logic:
Преобразуем его так, чтобы он мог хранить в себе не только логику, но и данные.
Чтобы класс мог хранить данные внутри него, надо объявить переменные. Переменные, объявленные внутри класса, но вне функций класса называются поля (англ. fields). Делается это так:
Теперь, чтобы была возможность передать данные классы и подцепить их к полю numbers, необходимо создать так называемый конструктор. Конструктор — это особый метод, который имеет то же имя, что и класс (в нашем случае Logic) и находится внутри класса. Добавим его.
Возникает закономерный вопрос, а зачем мы это сделали. Какое-то поле добавили… какой-то конструктор. А сделали мы это затем, чтобы избавится от параметров при вызове функций getSum, getProduct, getAverage.
И так, правим наши функции:
На что стоит обратить внимание:
- При обращении к полю или методу, принадлежащую тому же классу, в котором находится функция, мы используем ключевое слово this.
- например this.numbersField – обращение к полю numbersField
- this.getSum() – вызов функции getSum
- Убрали ключевое слово static. Сделали это затем, чтобы наши функции могли взаимодействовать с полем numbersField. В тоже время, из-за этого функции больше нельзя будет вызывать через Logic.getSomething.
- Вообще говоря, использование ключевого слово this опционально, и все this.numbersField можно заменить на numbersField, а this.getSum() на просто getSum(). Но я для использую его, чтобы акцентировать, что это не какой-то сферический numbersField из вакуума, а именно поле numbersField из класса Logic. То есть this внутри любого класса это ссылка на самого себя. Ну и да, this нельзя использовать в функциях, объявленных с ключевым словом static.
Подкорректируем наш класс Program, тут как раз пригодится наш конструктор. Вот что получится:
Если простая декомпозиция позволяла отделить логику программы от общения с пользователем. То использование класса, позволила нам, в дополнение, намертво привязать данные к логике. И таким образом реализовать парадигму “Индивидуальной задачи”. То есть класс Logic будучи решателем “Массовой задачи” при создании своего экземпляра через:
превращается в решатель “Индивидуальной задачи”. Это очень интересная метаморфоза, даже если вам так и не кажется =)
Ну и кроме того, использование конструктора избавило нас от необходимости передавать параметры numbers на каждый чих.
В общем, вот такой код:
с некоторой точки зрения, куда красивее чем такой:
Как тестировать
А также как раньше, только теперь необходимо создавать экземпляр класса. То есть если по старинке мы тестировали бы нашу программу так:
то теперь будем писать так
Итоговый код
Если вы вдруг запутались на каком-то этапе, то вот что у вас должно получиться: