Как работать с классами
- Задача
- Массовые и индивидуальные задачи
- Комплексная массовая задача
- Создаем класс для решателя индивидуальных задач
- Как тестировать
- Итоговый код
С ростом сложности программы, возникает необходимость в построении более продуманной архитектуры приложения.
Для написания тестов мы уже столкнулись с необходимостью разбить код на части ответственные за общение с пользователем и за часть ответственную непосредственно за логику программу.
На практике часто встречаются задачи, которые для одних и тех же данных требуются рассчитать сразу несколько результатов.
Для таких задач оказывается удобно привязывать данные непосредственно к обработчику.
Задача
Дан вектор чисел . Рассчитать
- значение суммы этих чисел
- значение произведения этих чисел
- рассчитать среднее арифметическое этих чисел
Данную задачу можно было решить так:
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var numbers = new int[] {1,2,3,4,5};
int sum = 0; // сумма
int product = 1; // произведение
double average = 0; // среднее
foreach (var el in numbers)
{
sum += el;
product *= el;
}
average = (double)sum / numbers.Length;
Console.WriteLine("Сумма {0}", sum);
Console.WriteLine("Произведение {0}", product);
Console.WriteLine("Среднее значение {0}", average);
Console.ReadKey();
}
}
}
Выполним декомпозицию, чтобы отделить логику от общения с пользователем. У нас три результат, следовательно, потребуется три функции.
public class Logic
{
// Функция для расчета суммы элементов
public static int getSum(int[] numbers)
{
int sum = 0;
foreach (var el in numbers)
{
sum += el;
}
return sum;
}
// Функция для расчета произведения элементов
public static int getProduct(int[] numbers)
{
if (numbers.Length == 0) // если массив пустой возвращаем ноль
{
return 0;
}
int product = 1;
foreach (var el in numbers)
{
product *= el;
}
return product;
}
// Функция для расчета среднего значения
public static double getAverage(int[] numbers)
{
// тут удачно вызовем уже реализованный нами метод, чтобы подсчитать сумму
var sum = Logic.getSum(numbers);
return (double)sum / numbers.Length;
}
}
class Program
{
static void Main(string[] args)
{
var numbers = new int[] {1,2,3,4,5};
// используем код вынесенный в отдельный класс
int sum = Logic.getSum(numbers); // сумма
int product = Logic.getProduct(numbers); // произведение
double average = Logic.getAverage(numbers); // среднее
Console.WriteLine("Сумма {0}", sum);
Console.WriteLine("Произведение {0}", product);
Console.WriteLine("Среднее значение {0}", average);
Console.ReadKey();
}
}
}
Выполнив декомпозицию, мы сделали нашу программу тестируемой. То есть для неё можно писать тесты, что есть хорошо.
Но пока код класса Logic, по сути, представляет собой просто набор функции, которые в силу особенностей языка C# нельзя хранить отдельно от класса (то есть каждая функция должна быть в каком-нибудь классе).
Такое архитектура программы в целом соответствует принципам структурного программирования. В том же языке C или C++, такой код выглядел бы попроще, никаких классов, никаких static-ов – только функции.
Но так как мы используем C#, которые дают нам использовать всю мощь объектно-ориентированного программирования, мы проползем по лестнице абстракции чуть выше.
Массовые и индивидуальные задачи
Немного формальной математики.
И так. В теории алгоритмов есть такое понятие как “Массовая задача” и “Индивидуальная задача”. Например,
-
Решить квадратное уравнение , это пример “Массовой задачи”.
-
А решить эту же задачу, при a = 1, b = 2, c = 1 это уже пример “Индивидуальной задачи”.
То есть в случае “Массовой задачи”, в задачи присутствует некоторая доля неопределенности, которая не позволяет получить ответ в виде набора констант (строковых, или числовых, или еще каких-то). То есть все знают формулы расчета корней квадратного уравнения, но как известно в зависимости от коэффициентов, полученные значения корней могут принимать какие угодно значения.
В “Индивидуальной задаче” неопределенность отсутствует, и ответ может быть получен строго определенно в виде набора констант.
Еще несколько примеров массовых и индивидуальных задач.
- Массовая задача: “Сложить числа a + b”, Индивидуальная задача: “сложить 2 + 2”
- Массовая задача: Подсчитать сумму элементов вектора . Индивидуальная задача: Подсчитать сумму элементов вектора
Как можно наблюдать всякая Массовая задача содержит в себе [бес]конечное количество индивидуальных задач.
Всякую задачу из лабораторной можно и нужно рассматривать как “Массовую задачу”. А тесты, которые мы для нее описываем можно рассматривать как некоторый набор индивидуальных задач.
Комплексная массовая задача
Вспомним как у нас выглядела исходная задача:
Дан вектор чисел . Рассчитать
- значение суммы этих чисел
- значение произведения этих чисел
- рассчитать среднее арифметическое этих чисел
Данную задачу можно рассматривать как комплексную массовую задачу.
То есть неопределенность у нас запрятана в фразе “Дан вектор чисел ”. А необходимые для расчета значения можно рассматривать как набор массовых подзадач. Эти значения можно рассчитать по следующим формулам:
– значение суммы
– значение произведения
– среднее арифметическое
Всякой “комплексной массовой задаче” можно сопоставить “комплексную индивидуальную задачу”.
То есть вариант когда дан вектор , для которого надо рассчитать сумму, произведение и среднее значение есть пример “комплексной индивидуальной задаче”.
К чему я все это? А к тому чтобы было бы удобно если бы мы могли объявить в коде программы класс в C#, которые являл бы собой олицетворение индивидуальной задачи. То есть содержал не только логику для расчета значений, но и сам набор значений, для которых он вызывается.
Попытка построить такую сущность есть пример использования объектно-ориентированного подхода в написании приложений. В общем, идем дальше.
Создаем класс для решателя индивидуальных задач
Вспомним код, который у нас был, а точнее, код класса Logic:
public class Logic
{
// Функция для расчета суммы элементов
public static int getSum(int[] numbers)
{
int sum = 0;
foreach (var el in numbers)
{
sum += el;
}
return sum;
}
// Функция для расчета произведения элементов
public static int getProduct(int[] numbers)
{
if (numbers.Length == 0) // если массив пустой возвращаем ноль
{
return 0;
}
int product = 1;
foreach (var el in numbers)
{
product *= el;
}
return product;
}
// Функция для расчета среднего значения
public static double getAverage(int[] numbers)
{
// тут удачно вызовем уже реализованный нами метод, чтобы подсчитать сумму
var sum = Logic.getSum(numbers);
return (double)sum / numbers.Length;
}
}
Преобразуем его так, чтобы он мог хранить в себе не только логику, но и данные.
Чтобы класс мог хранить данные внутри него, надо объявить переменные. Переменные, объявленные внутри класса, но вне функций класса называются поля (англ. fields). Делается это так:
public class Logic
{
int[] numbersField; // объявили поле для хранения исходных данных индивидуальной задачи
// это старый код, его не трогаем...
public static int getSum(int[] numbers){ /*...*/}
public static int getProduct(int[] numbers) { /*...*/ }
public static double getAverage(int[] numbers) { /*...*/ }
}
Теперь, чтобы была возможность передать данные классы и подцепить их к полю numbers, необходимо создать так называемый конструктор. Конструктор — это особый метод, который имеет то же имя, что и класс (в нашем случае Logic) и находится внутри класса. Добавим его.
public class Logic
{
int[] numbersField;
/*
* Объявили конструктор,
* которые в качестве параметра принимает array
*/
public Logic(int[] array)
{
// подцепляем массив array к полю numbers
this.numbersField = array;
}
// это старый код, его не трогаем...
public static int getSum(int[] numbers){ /*...*/}
public static int getProduct(int[] numbers) { /*...*/ }
public static double getAverage(int[] numbers) { /*...*/ }
}
Возникает закономерный вопрос, а зачем мы это сделали. Какое-то поле добавили… какой-то конструктор. А сделали мы это затем, чтобы избавится от параметров при вызове функций getSum, getProduct, getAverage.
И так, правим наши функции:
public class Logic
{
int[] numbersField;
public Logic(int[] array)
{
this.numbersField = array;
}
public int getSum() // тут убрал ключевое слово static и аргумент
{
int sum = 0;
foreach (var el in this.numbersField) // тут заменил numbers на this.numbersField
{
sum += el;
}
return sum;
}
public int getProduct() // тут убрал ключевое слово static и аргумент
{
if (this.numbersField.Length == 0) // тут заменил numbers на this.numbersField
{
return 0;
}
int product = 1;
foreach (var el in this.numbersField) // тут заменил numbers на this.numbersField
{
product *= el;
}
return product;
}
public double getAverage() // тут убрал ключевое слово static и аргумент
{
// тут удачно вызовем уже реализованный нами метод, чтобы подсчитать сумму
var sum = this.getSum(); // тут заменил Logic.getSum(numbers) на this.getSum()
return (double)sum / this.numbersField.Length;
}
}
На что стоит обратить внимание:
- При обращении к полю или методу, принадлежащую тому же классу, в котором находится функция, мы используем ключевое слово 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, тут как раз пригодится наш конструктор. Вот что получится:
class Program
{
static void Main(string[] args)
{
var numbers = new int[] {1,2,3,4,5};
var logic = new Logic(numbers); // создаем экземпляр класса, собственно тут и происходит вызов метода конструктора
int sum = logic.getSum(); // тут меняем Logic.getSum(numbers)
int product = logic.getProduct(); // тут меняем Logic.getProduct(numbers)
double average = logic.getAverage(); // тут меняем Logic.getAverage(numbers)
Console.WriteLine("Сумма {0}", sum);
Console.WriteLine("Произведение {0}", product);
Console.WriteLine("Среднее значение {0}", average);
Console.ReadKey();
}
}
Если простая декомпозиция позволяла отделить логику программы от общения с пользователем. То использование класса, позволила нам, в дополнение, намертво привязать данные к логике. И таким образом реализовать парадигму “Индивидуальной задачи”. То есть класс Logic будучи решателем “Массовой задачи” при создании своего экземпляра через:
var logic = new Logic(numbers);
превращается в решатель “Индивидуальной задачи”. Это очень интересная метаморфоза, даже если вам так и не кажется =)
Ну и кроме того, использование конструктора избавило нас от необходимости передавать параметры numbers на каждый чих.
В общем, вот такой код:
var logic = new Logic(numbers);
int sum = logic.getSum();
int product = logic.getProduct();
double average = logic.getAverage();
с некоторой точки зрения, куда красивее чем такой:
int sum = Logic.getSum(numbers);
int product = Logic.getProduct(numbers);
double average = Logic.getAverage(numbers);
Как тестировать
А также как раньше, только теперь необходимо создавать экземпляр класса. То есть если по старинке мы тестировали бы нашу программу так:
[TestMethod()]
public void LogicTest()
{
var numbers = new int[] { 1, 2, 3, 4, 5};
Assert.AreEqual(15, Logic.getSum(numbers));
Assert.AreEqual(120, Logic.getProduct(numbers));
Assert.AreEqual(3, Logic.getAverage(numbers));
}
то теперь будем писать так
[TestMethod()]
public void LogicTest()
{
var logic = new Logic(new int[] { 1, 2, 3, 4, 5});
Assert.AreEqual(15, logic.getSum());
Assert.AreEqual(120, logic.getProduct());
Assert.AreEqual(3, logic.getAverage());
}
Итоговый код
Если вы вдруг запутались на каком-то этапе, то вот что у вас должно получиться:
public class Logic
{
int[] numbersField;
public Logic(int[] array)
{
this.numbersField = array;
}
public int getSum()
{
int sum = 0;
foreach (var el in this.numbersField)
{
sum += el;
}
return sum;
}
public int getProduct()
{
if (this.numbersField.Length == 0)
{
return 0;
}
int product = 1;
foreach (var el in this.numbersField)
{
product *= el;
}
return product;
}
public double getAverage()
{
var sum = this.getSum();
return (double)sum / this.numbersField.Length;
}
}
class Program
{
static void Main(string[] args)
{
var numbers = new int[] {1,2,3,4,5};
var logic = new Logic(numbers);
int sum = logic.getSum();
int product = logic.getProduct();
double average = logic.getAverage();
Console.WriteLine("Сумма {0}", sum);
Console.WriteLine("Произведение {0}", product);
Console.WriteLine("Среднее значение {0}", average);
Console.ReadKey();
}
}