Интернет-журнал "Домашняя лаборатория", 2007 №9
Шрифт:
OneLinkList<int, string> list1 = new OneLinkList<int, string>;
OneLinkList<string, Person> list2 = new OneLinkList< string, Person>;
У списка list1 ключи имеют тип int, у списка list2 — string. Заметьте, оба фактических типа, согласно обязательствам, реализуют интерфейс IComparable, у первого списка тип элементов — string, у второго — Person. Все работает прекрасно. Вот результаты вычислений по этой
Как справиться с арифметикой
Представьте себе, что мы хотим иметь специализированный вариант нашего списка, элементы которого допускали бы операцию сложения и одно из полей которого сохраняло бы сумму всех элементов, добавленных в список. Как задать соответствующее ограничение на класс?
Как уже говорилось, наличие ограничения операции, где можно было бы указать, что над элементами определена операция +, решало бы проблему. Но такого типа ограничений нет. Хуже того, нет и интерфейса INumeric, аналогичного IComparable, определяющего метод сложения Add. Так что нам не может помочь и ограничение наследования.
Вот один из возможных выходов, предлагаемых в такой ситуации. Стратегия следующая: определим абстрактный универсальный класс Calc с методами, выполняющими вычисления. Затем создадим конкретизированных потомков этого класса. В классе, задающем список с суммированием, введем поле класса Calc. При создании экземпляров класса будем передавать фактические типы ключа и элементов, а также соответствующий калькулятор, но уже не как тип, а как аргумент конструктора класса. Этот калькулятор, согласованный с типом элементов, и будет выполнять нужные вычисления. Давайте приступим к реализации этой стратегии. Начнем с определения класса Calc;
public abstract class Calc<T>
{
public abstract T Add(T a, T b);
public abstract T Sub(T a, T b);
public abstract T Mult(T a, T b);
public abstract T Div(T a, T b);
}
Наш абстрактный универсальный класс определяет четыре арифметические операции. Давайте построим трех его конкретизированных потомков:
public class IntCalc: Calc<int>
{
public override int Add(int a, int b) { return (a + b);}
public override int Sub (int a, int b) { return (a — b);}
public override int Mult(int a, int b) { return (a * b);}
public override int Div(int a, int b) { return (a / b); }
}
public class DoubleCalc: Calc<double>
{
public override double Add(double a, double b)
{return (a + b);}
public override double Sub(double a, double b)
{return (a — b);}
public override double Mult(double a, double b)
{return (a * b);}
public override double Div(double a, double b)
{return (a / b);}
}
public class StringCalc: Calc<string>
{
public override string Add(string a, string b)
{return (a + b);}
public override string Sub(string a, string b)
{return (a);}
public override string Mult(string a, string b)
{return (a);}
public override string Div (string a, string b)
{return (a);}
}
Здесь
определяются три разных калькулятора: один — над целочисленными данными, другой — над данными с плавающей точкой, третий — над строковыми данными. В последнем случае определена, по сути, только операция сложения строк (конкатенации).Теперь нам нужно ввести изменения в ранее созданный класс OneLinkList. Обратите внимание на важный технологический принцип работы с объектными системами. Пусть уже есть нормально работающий класс с нормально работающими клиентами класса. Не следует изменять этот класс. Класс закрыт для изменений. Используйте наследование и открывайте класс-потомок, в который и вносите изменения, учитывающие добавляемую специфику класса. Принцип "Закрыт — Открыт" является одним из важнейших принципов построения программных систем в объектном стиле.
В полном соответствии с этим принципом построим класс SumList — потомок класса OneLinkList. То, что родительский класс является универсальным, ничуть не мешает строить потомка класса, сохраняющего универсальный характер родителя.
public class SumList<K, Т>: OneLinkList<K, Т> where К:
IComparable<K>
{
Calc<T> calc;
T s um;
public SumList(Calc<T> calc)
{ this.calc = calc; sum = default(T); }
public new void add(K key, T item)
{
Node<K, T> newnode = new Node<K, T>;
if (first == null)
{
first = newnode; cursor = newnode;
newnode.key = key; newnode.item = item;
sum = calc.Add(sum, item);
}
else
{
newnode.next = cursor.next; cursor.next = newnode;
newnode.key = key; newnode.item = item;
sum = calc.Add(sum, item);
}
}
public T Sum
{return (sum); }
}//SumList
У класса добавилось поле sum, задающее сумму хранимых элементов, и поле calc — калькулятор, выполняющий вычисления. Метод add, объявленный в классе с модификатором new, скрывает родительский метод add, задавая собственную реализацию этого метода. Родительский метод можно было бы определить как виртуальный, переопределив его у потомка, но я не стал трогать код родительского класса. К классу добавился еще один запрос, возвращающий значение поля sum.
Некоторые изменения в уже существующем проекте пришлось-таки сделать, изменив статус доступа у полей. А все потому, что в целях экономии текста кода я не стал закрывать поля и вводить, как положено, открытые процедуры-свойства для закрытых полей.
Проведем теперь эксперименты с новыми вариантами списков, допускающих суммирование элементов:
public void TestSum
{
SumList<string, int> list1 =
new SumList<string, int>(new IntCalc);
list1.add("Петр", 33); list1.add("Павел", 44);