Пример: GeometricShapes

Пример приложения GeometricShapes демонстрирует, как ряд понятий и возможностей объектно-ориентированного программирования можно применить с использованием ActionScript 3.0, включая:

  • определение классов,

  • расширение классов,

  • полиморфизм и ключевое слово override ,

  • определение, расширение и реализацию интерфейсов.

Кроме того, в примере используется «фабричный метод», который создает экземпляры классов, объявляя в качестве возвращаемого значения экземпляр интерфейса, и использует возвращенный объект в общем виде.

Получить файлы приложения для этого примера можно на странице www.adobe.com/go/learn_programmingAS3samples_flash_ru . Файлы приложения GeometricShapes находятся в папке Samples/GeometricShapes. Приложение состоит из следующих файлов.

Файл

Описание

GeometricShapes.mxml

или

GeometricShapes.fla

Основной файл приложения Flash (FLA) или Flex (MXML).

com/example/programmingas3/geometricshapes/IGeometricShape.as

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

com/example/programmingas3/geometricshapes/IPolygon.as

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

com/example/programmingas3/geometricshapes/RegularPolygon.as

Тип геометрической фигуры, равные стороны которого симметрично противоположны относительно центра фигуры.

com/example/programmingas3/geometricshapes/Circle.as

Тип геометрической фигуры, определяющей круг.

com/example/programmingas3/geometricshapes/EquilateralTriangle.as

Подкласс класса RegularPolygon, определяющий равносторонний треугольник.

com/example/programmingas3/geometricshapes/Square.as

Подкласс класса RegularPolygon, определяющий равносторонний прямоугольник.

com/example/programmingas3/geometricshapes/GeometricShapeFactory.as

Класс, содержащий фабричный метод для создания фигур заданного типа и размера.

Определение классов GeometricShapes

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

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

Это приложение работает с геометрическими фигурами, но не отображает их графически.

Классы и интерфейсы, определяющие геометрические фигуры в этом примере, представлены на следующей схеме с использованием нотации UML (Унифицированный язык моделирования).

Полноразмерное изображение
Классы в примере GeometricShapes

Определение общего поведения с помощью интерфейсов

Это приложение GeometricShapes работает с тремя типами фигур: кругами, квадратами и равносторонними треугольниками. Структура классов GeometricShapes начинается с очень простого интерфейса, IGeometricShape, в котором перечислены методы, общие для фигур всех трех типов.

package com.example.programmingas3.geometricshapes 
{ 
    public interface IGeometricShape 
    { 
        function getArea():Number; 
        function describe():String; 
    } 
}

Интерфейс определяет два метода: метод getArea() , который вычисляет и возвращает площадь фигуры, и метод describe() , который генерирует текстовое описание свойств фигуры.

Также желательно знать периметр каждой фигуры. Однако периметр круга называется окружностью, он вычисляется по особой формуле, поэтому его поведение отличается от поведения треугольника и квадрата. Несмотря на это, между треугольниками, квадратами и другими многоугольниками существует достаточно большое сходство, чтобы для них можно было определить новый класс интерфейса: IPolygon. Интерфейс IPolygon также довольно простой, как показано ниже.

package com.example.programmingas3.geometricshapes 
{ 
    public interface IPolygon extends IGeometricShape 
    { 
        function getPerimeter():Number; 
        function getSumOfAngles():Number; 
    } 
}

Этот интерфейс определяет два метода, общие для всех многоугольников: метод getPerimeter() , который изменяет совокупный размер всех сторон, и метод getSumOfAngles() , который вычисляет сумму всех внутренних углов.

Интерфейс IPolygon расширяет интерфейс IGeometricShape, то есть любой класс, реализующий интерфейс IPolygon, должен объявить все четыре метода: два из IGeometricShape и два из IPolygon.

Определение классов фигур

Разобравшись с методами, общими для каждого типа фигур, можно определить сами классы фигур. С точки зрения количества реализуемых методов, самой простой фигурой является круг (класс Circle), как показано ниже.

package com.example.programmingas3.geometricshapes 
{ 
    public class Circle implements IGeometricShape 
    { 
        public var diameter:Number; 
         
        public function Circle(diam:Number = 100):void 
        { 
            this.diameter = diam; 
        } 
         
        public function getArea():Number 
        { 
            // The formula is Pi * radius * radius. 
            var radius:Number = diameter / 2; 
            return Math.PI * radius * radius; 
        } 
         
        public function getCircumference():Number 
        { 
            // The formula is Pi * diameter. 
            return Math.PI * diameter; 
        } 
         
        public function describe():String 
        { 
            var desc:String = "This shape is a Circle.\n"; 
            desc += "Its diameter is " + diameter + " pixels.\n"; 
            desc += "Its area is " + getArea() + ".\n"; 
            desc += "Its circumference is " + getCircumference() + ".\n"; 
            return desc; 
        } 
    } 
}

Класс Circle реализует интерфейс IGeometricShape, поэтому он должен включать код для методов getArea() и describe() . Помимо этого, определяется метод getCircumference() , уникальный для класса Circle. Класс Circle также объявляет свойство diameter , которого нет у классов многоугольников, так как оно содержит значение диаметра.

Остальные два типа фигур (квадраты и равносторонние треугольники) имеют больше общих свойств: все их стороны равны, а для расчета их периметра и суммы внутренних углов используются общие формулы. В действительности эти распространенные формулы применяются и к другим правильным многоугольникам, которые вам также придется определять в будущем.

Класс RegularPolygon является суперклассом для классов Square и EquilateralTriangle. В суперклассе можно определить все общие методы, чтобы их не нужно было определять по отдельности в каждом подклассе. Ниже приводится код класса RegularPolygon:

package com.example.programmingas3.geometricshapes 
{ 
    public class RegularPolygon implements IPolygon 
    { 
        public var numSides:int; 
        public var sideLength:Number; 
         
        public function RegularPolygon(len:Number = 100, sides:int = 3):void 
        { 
            this.sideLength = len; 
            this.numSides = sides; 
        } 
         
        public function getArea():Number 
        { 
            // This method should be overridden in subclasses. 
            return 0; 
        } 
         
        public function getPerimeter():Number 
        { 
            return sideLength * numSides; 
        } 
         
        public function getSumOfAngles():Number 
        { 
            if (numSides >= 3) 
            { 
                return ((numSides - 2) * 180); 
            } 
            else 
            { 
                return 0; 
            } 
        } 
         
        public function describe():String 
        { 
            var desc:String = "Each side is " + sideLength + " pixels long.\n"; 
            desc += "Its area is " + getArea() + " pixels square.\n"; 
            desc += "Its perimeter is " + getPerimeter() + " pixels long.\n";  
            desc += "The sum of all interior angles in this shape is " + getSumOfAngles() + " degrees.\n";  
            return desc; 
        } 
    } 
}

В первую очередь, класс RegularPolygon объявляет два свойства, общие для всех правильных многоугольников: длину каждой стороны (свойство sideLength ) и количество сторон (свойство numSides ).

Класс RegularPolygon реализует интерфейс IPolygon и объявляет все четыре метода интерфейса IPolygon. Два из них, getPerimeter() и getSumOfAngles() , реализуются с помощью общих формул.

Так как для каждой фигуры используется своя формула метода getArea() , версия базового класса метода не может содержать общую логику, которую могли бы наследовать методы подклассов. Вместо этого возвращается значение по умолчанию 0, указывающее на то, что площадь не вычислена. Чтобы правильно вычислить площадь каждой фигуры, подклассы класса RegularPolygon должны самостоятельно переопределять метод getArea() .

Следующий код для класса EquilateralTriangle демонстрирует переопределение метода getArea() .

package com.example.programmingas3.geometricshapes  
{ 
    public class EquilateralTriangle extends RegularPolygon 
    { 
        public function EquilateralTriangle(len:Number = 100):void 
        { 
            super(len, 3); 
        } 
         
        public override function getArea():Number 
        { 
        // The formula is ((sideLength squared) * (square root of 3)) / 4. 
        return ( (this.sideLength * this.sideLength) * Math.sqrt(3) ) / 4; 
        } 
         
        public override function describe():String 
        { 
                 /* starts with the name of the shape, then delegates the rest 
                 of the description work to the RegularPolygon superclass */ 
        var desc:String = "This shape is an equilateral Triangle.\n"; 
        desc += super.describe(); 
        return desc; 
        } 
    } 
}

Ключевое слово override указывает на то, что метод EquilateralTriangle.getArea() намеренно переопределяет метод getArea() суперкласса RegularPolygon. Когда вызывается метод EquilateralTriangle.getArea() , он вычисляет площадь по формуле из предыдущего кода, а код RegularPolygon.getArea() никогда не выполняется.

Класс EquilateralTriangle не определяет собственную версию метода getPerimeter() . Когда вызывается метод EquilateralTriangle.getPerimeter() , этот вызов передается вверх по цепочке наследования и выполняет под в методе getPerimeter() суперкласса RegularPolygon.

Конструктор EquilateralTriangle() использует инструкцию super() , чтобы сделать явный вызов конструктора RegularPolygon() своего суперкласса. Если бы оба конструктора имели одинаковый набор параметров, можно было бы совсем опустить конструктор EquilateralTriangle() , а вместо этого вызывался бы конструктор RegularPolygon() . Однако конструктору RegularPolygon() требуется дополнительный параметр, numSides . Поэтому конструктор EquilateralTriangle() вызывает метод super(len, 3) , который передает параметр ввода len и значение 3, указывающее на то, что у фигуры будет три стороны.

В методе describe() также используется оператор super() , но другим способом. Он используется для вызова версии суперкласса RegularPolygon метода describe() . Метод EquilateralTriangle.describe() сначала задает в качестве значения строковой переменной desc предложение, описывающее тип фигуры. Затем он получает результаты метода RegularPolygon.describe() , вызвав super.describe() , и добавляет результат в строку desc .

Здесь не приводится подробное описание класса Square, но он похож на класс EquilateralTriangle, в котором есть конструктор и собственные версии методов getArea() и describe() .

Полиморфизм и фабричный метод

Набор классов, в которых используются интерфейсы и наследования, может применяться для достижения разных интересных целей. Например, все классы фигур, описанные выше, либо реализуют интерфейс IGeometricShape, либо расширяют реализующий его суперкласс. Поэтому если переменная определена в качестве экземпляра IGeometricShape, совершенно не нужно знать, является ли она на самом деле экземпляром класса Circle или Square, чтобы вызвать ее метод describe() .

Следующий код демонстрирует, как это происходит.

var myShape:IGeometricShape = new Circle(100); 
trace(myShape.describe());

Когда вызывается метод myShape.describe() , он выполняет метод Circle.describe() , так как несмотря на то, что переменная определена как экземпляр IGeometricShape, ее базовым классом является Circle.

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

Приложение GeometricShapes применяет этот тип полиморфизма на базе интерфейса, используя упрощенную версию шаблона, известного под названием «фабричный метод». Термин фабричный метод обозначает функцию, которая возвращает объект, базовый тип данных или содержимое которого могут быть разными в зависимости от контекста.

Описанный здесь класс GeometricShapeFactory определяет фабричный метод с именем createShape() .

package com.example.programmingas3.geometricshapes  
{ 
    public class GeometricShapeFactory  
    { 
        public static var currentShape:IGeometricShape; 
 
        public static function createShape(shapeName:String,  
                                        len:Number):IGeometricShape 
        { 
            switch (shapeName) 
            { 
                case "Triangle": 
                    return new EquilateralTriangle(len); 
 
                case "Square": 
                    return new Square(len); 
 
                case "Circle": 
                    return new Circle(len); 
            } 
            return null; 
        } 
     
        public static function describeShape(shapeType:String, shapeSize:Number):String 
        { 
            GeometricShapeFactory.currentShape = 
                GeometricShapeFactory.createShape(shapeType, shapeSize); 
            return GeometricShapeFactory.currentShape.describe(); 
        } 
    } 
}

Фабричный метод createShape() позволяет конструкторам подклассов фигур определять детали создаваемых ими экземпляров и возвращать новые объекты в качестве экземпляров IGeometricShape, чтобы они проходили более общую обработку в приложении.

Метод describeShape() в предыдущем примере демонстрирует, как приложение может использовать фабричный метод для получения общей ссылки на более конкретный объект. Приложение может получить описание для только что созданного объекта Circle, как показано ниже.

GeometricShapeFactory.describeShape("Circle", 100);

Затем метод describeShape() вызывает фабричный метод createShape() с теми же параметрами, сохраняя новый объект Circle в статической переменной currentShape , которая была задана в качестве объекта IGeometricShape. После этого вызывается метод describe() объекта currentShape , и в результате разрешения этот метод автоматически выполняет метод Circle.describe() , возвращая подробное описание круга.

Расширение примера приложения

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

Допустим, требуется добавить новую фигуру, пятиугольник, в этот пример приложения. Для этого нужно создать класс Pentagon, который расширяет класс RegularPolygon и определяет собственные версии методов getArea() и describe() . После этого следует добавить новый элемент «Pentagon» (Пятиугольник) в комбинированное поле пользовательского интерфейса приложения. Вот и все! Класс Pentagon автоматически получает методы getPerimeter() и getSumOfAngles() , наследуя их от класса RegularPolygon. Так как класс Pentagon является потомком класса, который реализует интерфейс IGeometricShape, его экземпляр также можно создать в качестве экземпляра IGeometricShape. Это означает, что для добавления нового типа фигуры не требуется изменять сигнатуру методов класса GeometricShapeFactory (и, следовательно, не требуется изменять коды, использующие класс GeometricShapeFactory).

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