Задача
Написать программу, которая отображает движущиеся с постоянной скоростью прямоугольники и квадраты. В процессе движения, фигуры должны менять свои размеры и отображать текущую площадь.
Объекты
.Для решения задачи понадобятся следующие объекты.
- Холст для рисования
- Квадраты
- Прямоугольники
Решение
Подумав немного, вспоминаем, что вроде как квадрат, это прямоугольник с одинаковыми сторонами. Чтобы обеспечить одинаковость сторон, при установлении их длины будем менять и длину и высоту. Исходя из этих соображений строим следующую иерархию классов:
class Rectangle {
. Point Anchor{ get; set; }
. int Width { get; set; }
. int Height { get; set; }
. int GetArea() { return Width*Height; }
}
class Square : Rectangle {
. int Width { get; set(int value) { Width = value; Height = value; } }
. int Height { get; set(int value) { Width = value; Height = value; }; }
}
Ура! Мы повторно использовали метод GetArea, прочувствовав мощь наследования. Теперь займемся полиморфизмом. У нас есть холст для рисования. Напишем метод Paint, который будет получать список фигур, и рисовать их, изменяя положение на dx, dy, а размеры на dw, dh, со случайным знаком.
class Canvas {
. void Paint(List<Rectagle> shapes) {
. . foreach(shape in shapes) {
. . . shape.Anchor.Move(dx, dy);
. . . shape.Height += dh*RandomSign();
. . . shape.Width += dw*RandomSign();
. . . DrawRectangle(shape.Anchor, shape.Width, shape.Height);
. . . DrawText(shape.Anchor, shape.GetArea().ToString);
. . } } }
Однако, запустив нашу программу, замечаем, что квадраты почему-то изменяются только на dw, полностью игнорируя dh. Проверив код убеждаемся -- дело в том, что для квадратов мы изменяем размеры дважды, сначала на dh, а потом на dw. В чем же ошибка?
Синдром квадрата
Ошибка в неверном предположении, что прямоугольник может быть квадратом. Мы мало думали и взяли несущественный признак -- равенство сторон и не учли существенное различие -- у прямоугольника две стороны, а у квадрата одна. Когда прямоугольник имеет равные стороны, он лишь выглядит как квадрат, но не становится им. Эти фигуры имеют разные модели. Поэтому, когда мы пытаемся произвести квадрат из прямоугольника, то мы ломаем модель прямоугольника. Наследник может добавлять параметры к состоянию, но он не должен "удалять" их.
Исправляем
Помимо неверного наследования, в первоначальном решении классы прямоугольника и квадрата имеют слишком открытый интерфейс, а холст для рисования слишком много о них знает (недостаточная инкапсуляция). Поэтому помимо изменения иерархии сократим объем публичной информации. Начнем с определения фигуры. Наш метод Paint выполняет с фигурой три действия: перемещение, изменение размеров и отрисовку. Вот и добавим эти действия в интерфейс фигуры:
inteface IShape {
. void Move();
. void Animate();
. void Paint(ICanvas canvas);
}
Перемещение и изменение размеров зависят только от состояния фигуры, поэтому параметры им не нужны, а вот отрисовке нужен еще и холст. Так что его будем передавать, но уже как интерфейс.
С учетом интерфейса IShape, метод Paint холста существенно упростится.
class Canvas : ICanvas {
. void Paint(List<IShape> shapes) {
. . foreach(shape in shapes) {
. . . shape.Move();
. . . shape.Animate();
. . . shape.Paint(self);
. . } }
}
Ради этой простоты, я не стал использовать I из SOLID (зато L во всей красе). В задаче у нас все фигуры движутся и анимируются. Появятся ли статические не известно, а разделение интерфейса усложнит метод отрисовки. То же можно сказать и про S. Перемещение и анимация размеров сами по себе в текущей задаче не нужны, но влияют на отрисовку. Вот и пусть лежат рядом.
Теперь займемся фигурами. Перемещение квадрата не отличается от перемещения прямоугольника -- мы просто изменяем якорную точку на (dx, dy). Поэтому можем объединить этот функционал в отдельном классе -- подвижной фигуре.
class MobileShape {
. protected Point anchor;
. void MobileFigure(Point asnchor) {...}
. void Move() { anchor.Move(dx, dy); }
}
Заодно капнули на мельницу S из SOLID.
Теперь все готово для реализации квадратов и прямоугольников. Оба класса будут наследовать от подвижной фигуры и реализовывать интерфейс фигуры. Реализация интерфейса здесь разделенная. Метод перемещения придет из подвижной фигуры а методы изменения и отрисовки каждый класс реализует самостоятельно.
class Square : MobileShape, IShape {
. private int side;
. void Square(Point anchor, int side) base(anchor) {...}
. void Animate() { side += ds*RandomSign(); }
. void Paint(ICanvas canvas) {
. . canvas.DrawRectangle(anchor, side, side);
. . canvas.DrawText(anchor, (side*side).ToString());
. . }
}
class Rectanle: MobileShape, IShape {
. private int width;
. private int height;
. void Rectangle(Point anchor, int width, int height) base(anchor) {...}
. void Animate() { width += dw*RandomSign(); height += dh*RandomSign(); }
. void Paint(ICanvas canvas) {
. . canvas.DrawRectangle(anchor, width, height);
. . canvas.DrawText(anchor, (width*height).ToString());
. . }
}
Бросается в глаза некоторая схожесть методов Paint. Но эта схожесть не существенна и случайна (повезло, что для отрисовки можно использовать один и тот же метод холста). Гораздо более существенны различия в использовании параметров. И еще больше они возрастут, когда придется рисовать, например, треугольник.
А делить не будем. Пусть S из SOLID опять отдохнет. Такое решение будет последовательным: поскольку если я не стал делить интерфейс, то зачем делить реализацию?
Заключение
При проектировании классов оценивайте отношения между ними по шкале существенное/не существенное. Учитывайте существенные отношения и отбрасывайте не существенные.