Skip to content
IRC-CodingIRC-Coding
UMLPolymorphismдинамическая привязкапереопределениеперегрузкаGenericsOOPнаследование

Основы Polymorphism в UML: динамическая привязка

Polymorphism в UML с динамической привязкой, переопределением и перегрузкой. Примеры на Java.

S

schutzgeist

14 min read
Основы Polymorphism в UML: динамическая привязка

Основы полиморфизма в UML: динамическая диспетчеризация и переопределение методов

Полиморфизм — ключевое понятие объектно-ориентированного программирования и моделирования в UML. Он позволяет объектам разных классов одинаково реагировать на одни и те же сообщения, но с разными результатами.

Что такое полиморфизм?

Полиморфизм (множественность форм) описывает способность объектов использовать одинаковый интерфейс, но с различными реализациями. В UML это представляется через иерархии наследования и интерфейсы.

Виды полиморфизма

  1. Переопределение (Override): метод подкласса заменяет метод базового класса
  2. Перегрузка (Overload): несколько методов с одинаковым названием, но разными параметрами
  3. Параметрический полиморфизм: обобщённые типы для типобезопасного переиспользования кода
  4. Специальный полиморфизм: перегрузка методов и преобразование типов

Представление полиморфизма в UML

Диаграмма классов с полиморфизмом

@startuml
abstract class Shape {
    -color: String
    -x: double
    -y: double
    +Shape(color: String, x: double, y: double)
    +move(dx: double, dy: double): void
    +area(): double {abstract}
    +perimeter(): double {abstract}
    +toString(): String
}

class Rectangle {
    -width: double
    -height: double
    +Rectangle(color: String, x: double, y: double, width: double, height: double)
    +area(): double
    +perimeter(): double
    +setDimensions(width: double, height: double): void
    +toString(): String
}

class Circle {
    -radius: double
    +Circle(color: String, x: double, y: double, radius: double)
    +area(): double
    +perimeter(): double
    +setRadius(radius: double): void
    +toString(): String
}

class Triangle {
    -base: double
    -height: double
    +Triangle(color: String, x: double, y: double, base: double, height: double)
    +area(): double
    +perimeter(): double
    +toString(): String
}

Shape <|-- Rectangle
Shape <|-- Circle
Shape <|-- Triangle

@enduml

Диаграмма последовательности для динамической диспетчеризации

@startuml
actor User
User -> ShapeProcessor: processShapes(shapes)
activate ShapeProcessor

loop для каждой фигуры
    ShapeProcessor -> Shape: area()
    activate Shape
    
    alt Rectangle
        Shape --> ShapeProcessor: Rectangle.area()
    else Circle
        Shape --> ShapeProcessor: Circle.area()
    else Triangle
        Shape --> ShapeProcessor: Triangle.area()
    end
    
    deactivate Shape
    ShapeProcessor -> Shape: perimeter()
    activate Shape
    
    alt Rectangle
        Shape --> ShapeProcessor: Rectangle.perimeter()
    else Circle
        Shape --> ShapeProcessor: Circle.perimeter()
    else Triangle
        Shape --> ShapeProcessor: Triangle.perimeter()
    end
    
    deactivate Shape
end

ShapeProcessor --> User: Результаты
deactivate ShapeProcessor
@enduml

Динамическая диспетчеризация в Java

Переопределение методов и динамический вызов

public class PolymorphismDemo {
    
    // Абстрактный базовый класс
    public abstract class Shape {
        protected String color;
        protected double x, y;
        
        public Shape(String color, double x, double y) {
            this.color = color;
            this.x = x;
            this.y = y;
        }
        
        // Метод, который можно переопределить
        public void move(double dx, double dy) {
            this.x += dx;
            this.y += dy;
            System.out.println(color + " фигура сдвинута в (" + x + ", " + y + ")");
        }
        
        // Абстрактные методы, которые обязательно переопределить
        public abstract double area();
        public abstract double perimeter();
        
        // Конкретный метод, который можно переопределить
        public String getDescription() {
            return "Фигура " + color + " в позиции (" + x + ", " + y + ")";
        }
        
        // Геттеры
        public String getColor() { return color; }
        public double getX() { return x; }
        public double getY() { return y; }
    }
    
    // Прямоугольник, переопределяет абстрактные методы
    public class Rectangle extends Shape {
        private double width, height;
        
        public Rectangle(String color, double x, double y, double width, double height) {
            super(color, x, y);
            this.width = width;
            this.height = height;
        }
        
        @Override
        public double area() {
            return width * height;
        }
        
        @Override
        public double perimeter() {
            return 2 * (width + height);
        }
        
        @Override
        public String getDescription() {
            return super.getDescription() + " (прямоугольник " + width + "x" + height + ")";
        }
        
        // Дополнительный метод
        public void setDimensions(double width, double height) {
            this.width = width;
            this.height = height;
        }
    }
    
    // Круг, переопределяет абстрактные методы
    public class Circle extends Shape {
        private double radius;
        
        public Circle(String color, double x, double y, double radius) {
            super(color, x, y);
            this.radius = radius;
        }
        
        @Override
        public double area() {
            return Math.PI * radius * radius;
        }
        
        @Override
        public double perimeter() {
            return 2 * Math.PI * radius;
        }
        
        @Override
        public String getDescription() {
            return super.getDescription() + " (круг с радиусом " + radius + ")";
        }
        
        public void setRadius(double radius) {
            this.radius = radius;
        }
    }
    
    // Демонстрация динамической диспетчеризации
    public void demonstrateDynamicBinding() {
        List<Shape> shapes = new ArrayList<>();
        shapes.add(new Rectangle("красный", 0, 0, 5, 3));
        shapes.add(new Circle("синий", 10, 10, 2));
        shapes.add(new Rectangle("зелёный", 5, 5, 2, 2));
        
        // Полиморфная обработка — динамический вызов метода
        for (Shape shape : shapes) {
            System.out.println(shape.getDescription());
            
            // Динамическая диспетчеризация — в зависимости от типа объекта вызывается нужный метод
            double area = shape.area();        // Вызывает Rectangle.area() или Circle.area()
            double perimeter = shape.perimeter(); // Вызывает Rectangle.perimeter() или Circle.perimeter()
            
            System.out.println("  Площадь: " + String.format("%.2f", area));
            System.out.println("  Периметр: " + String.format("%.2f", perimeter));
            
            // move() тоже может быть переопределён
            shape.move(1, 1);
            System.out.println();
        }
    }
}

Перегрузка методов

Перегрузка в Java

public class MethodOverloading {
    
    // Перегруженные методы для разных типов параметров
    public class Calculator {
        
        // Перегрузка для int
        public int add(int a, int b) {
            System.out.println("int add(int, int) вызван");
            return a + b;
        }
        
        // Перегрузка для double
        public double add(double a, double b) {
            System.out.println("double add(double, double) вызван");
            return a + b;
        }
        
        // Перегрузка для трёх параметров
        public int add(int a, int b, int c) {
            System.out.println("int add(int, int, int) вызван");
            return a + b + c;
        }
        
        // Перегрузка для массива
        public int add(int[] numbers) {
            System.out.println("int add(int[]) вызван");
            int sum = 0;
            for (int num : numbers) {
                sum += num;
            }
            return sum;
        }
        
        // Перегрузка с переменным числом аргументов
        public int addVarargs(int... numbers) {
            System.out.println("int addVarargs(int...) вызван");
            return add(numbers);
        }
        
        // Перегрузка для разных типов объектов
        public String concatenate(String a, String b) {
            System.out.println("String concatenate(String, String) вызван");
            return a + b;
        }
        
        public String concatenate(String a, String b, String c) {
            System.out.println("String concatenate(String, String, String) вызван");
            return a + b + c;
        }
    }
    
    // Демонстрация перегрузки
    public void demonstrateOverloading() {
        Calculator calc = new Calculator();
        
        // Разные перегруженные версии вызываются автоматически
        System.out.println("5 + 3 = " + calc.add(5, 3));
        System.out.println("5.5 + 3.3 = " + calc.add(5.5, 3.3));
        System.out.println("1 + 2 + 3 = " + calc.add(1, 2, 3));
        System.out.println("Сумма массива = " + calc.add(new int[]{1, 2, 3, 4, 5}));
        System.out.println("Сумма varargs = " + calc.addVarargs(1, 2, 3, 4, 5));
        System.out.println("Hello + World = " + calc.concatenate("Hello", "World"));
        System.out.println("A + B + C = " + calc.concatenate("A", "B", "C"));
    }
}

Перегрузка с наследованием

public class OverloadingWithInheritance {
    
    public class Animal {
        public void makeSound() {
            System.out.println("Animal makes a sound");
        }
        
        public void makeSound(String intensity) {
            System.out.println("Animal makes a " + intensity + " sound");
        }
    }
    
    public class Dog extends Animal {
        @Override
        public void makeSound() {
            System.out.println("Dog barks");
        }
        
        // Перегрузка, не переопределение
        public void makeSound(String intensity) {
            System.out.println("Dog barks " + intensity);
        }
        
        // Дополнительная перегрузка
        public void makeSound(String intensity, int times) {
            for (int i = 0; i < times; i++) {
                System.out.println("Dog barks " + intensity);
            }
        }
    }
    
    public void demonstrateOverloadingInheritance() {
        Animal animal = new Animal();
        Dog dog = new Dog();
        Animal animalDog = new Dog(); // Upcasting
        
        // Статическая привязка при перегрузке (во время компиляции)
        animal.makeSound();           // Animal makes a sound
        animal.makeSound("loud");     // Animal makes a loud sound
        
        dog.makeSound();              // Dog barks (переопределено)
        dog.makeSound("loud");        // Dog barks loud (перегружено)
        dog.makeSound("loud", 3);     // Dog barks loud (3x) (перегружено)
        
        // Важно: статическая привязка при перегрузке!
        animalDog.makeSound();        // Dog barks (динамическая привязка)
        animalDog.makeSound("loud");  // Animal makes a loud sound (статическая привязка!)
    }
}

Generics и параметрический полиморфизм

Generic классы

public class GenericPolymorphism {
    
    // Generic контейнер
    public class Container<T> {
        private T content;
        private String label;
        
        public Container(String label, T content) {
            this.label = label;
            this.content = content;
        }
        
        public T getContent() {
            return content;
        }
        
        public void setContent(T content) {
            this.content = content;
        }
        
        public String getLabel() {
            return label;
        }
        
        // Generic метод
        public <U> Container<U> transform(Function<T, U> transformer) {
            U newContent = transformer.apply(content);
            return new Container<>(label, newContent);
        }
        
        @Override
        public String toString() {
            return label + ": " + content;
        }
    }
    
    // Generic процессор
    public class Processor<T> {
        public List<T> filter(List<T> items, Predicate<T> predicate) {
            return items.stream()
                .filter(predicate)
                .collect(Collectors.toList());
        }
        
        public <R> List<R> map(List<T> items, Function<T, R> mapper) {
            return items.stream()
                .map(mapper)
                .collect(Collectors.toList());
        }
        
        public T reduce(List<T> items, BinaryOperator<T> accumulator, T identity) {
            return items.stream()
                .reduce(identity, accumulator);
        }
    }
    
    // Демонстрация
    public void demonstrateGenerics() {
        // Контейнер с разными типами
        Container<String> stringContainer = new Container<>("Text", "Hello World");
        Container<Integer> intContainer = new Container<>("Zahl", 42);
        Container<List<String>> listContainer = new Container<>("Liste", 
            Arrays.asList("A", "B", "C"));
        
        System.out.println(stringContainer);
        System.out.println(intContainer);
        System.out.println(listContainer);
        
        // Трансформация с generic методом
        Container<Integer> lengthContainer = stringContainer.transform(String::length);
        System.out.println("Länge: " + lengthContainer);
        
        // Generic процессор
        Processor<String> stringProcessor = new Processor<>();
        List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
        
        // Фильтрация
        List<String> longWords = stringProcessor.filter(words, s -> s.length() > 5);
        System.out.println("Lange Wörter: " + longWords);
        
        // Преобразование
        List<Integer> lengths = stringProcessor.map(words, String::length);
        System.out.println("Längen: " + lengths);
        
        // Редукция
        String concatenated = stringProcessor.reduce(words, String::concat, "");
        System.out.println("Zusammengefügt: " + concatenated);
    }
}

Generic методы

public class GenericMethods {
    
    // Generic метод для сравнения
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
    
    // Generic метод для обмена
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // Generic метод для конвертации
    public static <T, R> List<R> convertList(List<T> list, Function<T, R> converter) {
        return list.stream()
            .map(converter)
            .collect(Collectors.toList());
    }
    
    // Generic метод с wildcards
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
    
    // Wildcard с верхней границей
    public static double sumOfNumbers(List<? extends Number> numbers) {
        return numbers.stream()
            .mapToDouble(Number::doubleValue)
            .sum();
    }
    
    // Wildcard с нижней границей
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
    
    // Демонстрация
    public static void demonstrateGenericMethods() {
        // Max метод
        System.out.println("Max von 5 und 3: " + max(5, 3));
        System.out.println("Max von 'Hello' und 'World': " + max("Hello", "World"));
        
        // Swap метод
        String[] words = {"A", "B", "C"};
        System.out.println("Vor Swap: " + Arrays.toString(words));
        swap(words, 0, 2);
        System.out.println("Nach Swap: " + Arrays.toString(words));
        
        // Convert метод
        List<String> strings = Arrays.asList("1", "2", "3", "4", "5");
        List<Integer> integers = convertList(strings, Integer::parseInt);
        System.out.println("Konvertiert: " + integers);
        
        // Wildcard методы
        List<String> stringList = Arrays.asList("A", "B", "C");
        List<Integer> intList = Arrays.asList(1, 2, 3);
        
        System.out.println("String Liste:");
        printList(stringList);
        
        System.out.println("Integer Liste:");
        printList(intList);
        
        // Wildcard с верхней границей
        List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
        System.out.println("Summe: " + sumOfNumbers(doubles));
        
        // Wildcard с нижней границей
        List<Number> numbers = new ArrayList<>();
        addNumbers(numbers);
        System.out.println("Zahlen hinzugefügt: " + numbers);
    }
}

Интерфейсы и полиморфизм

Полиморфизм на основе интерфейсов

public class InterfacePolymorphism {
    
    // Interface для полиморфного поведения
    public interface Drawable {
        void draw();
        double getArea();
        String getType();
    }
    
    public interface Movable {
        void move(double dx, double dy);
        void setPosition(double x, double y);
        double[] getPosition();
    }
    
    // Класс реализует несколько интерфейсов
    public class Circle implements Drawable, Movable {
        private double radius, x, y;
        
        public Circle(double radius, double x, double y) {
            this.radius = radius;
            this.x = x;
            this.y = y;
        }
        
        @Override
        public void draw() {
            System.out.println("Zeichne Kreis an (" + x + ", " + y + ") mit Radius " + radius);
        }
        
        @Override
        public double getArea() {
            return Math.PI * radius * radius;
        }
        
        @Override
        public String getType() {
            return "Kreis";
        }
        
        @Override
        public void move(double dx, double dy) {
            x += dx;
            y += dy;
            System.out.println("Kreis verschoben nach (" + x + ", " + y + ")");
        }
        
        @Override
        public void setPosition(double x, double y) {
            this.x = x;
            this.y = y;
        }
        
        @Override
        public double[] getPosition() {
            return new double[]{x, y};
        }
    }
    
    public class Rectangle implements Drawable, Movable {
        private double width, height, x, y;
        
        public Rectangle(double width, double height, double x, double y) {
            this.width = width;
            this.height = height;
            this.x = x;
            this.y = y;
        }
        
        @Override
        public void draw() {
            System.out.println("Zeichne Rechteck an (" + x + ", " + y + ") " + width + "x" + height);
        }
        
        @Override
        public double getArea() {
            return width * height;
        }
        
        @Override
        public String getType() {
            return "Rechteck";
        }
        
        @Override
        public void move(double dx, double dy) {
            x += dx;
            y += dy;
            System.out.println("Rechteck verschoben nach (" + x + ", " + y + ")");
        }
        
        @Override
        public void setPosition(double x, double y) {
            this.x = x;
            this.y = y;
        }
        
        @Override
        public double[] getPosition() {
            return new double[]{x, y};
        }
    }
    
    // Полиморфная обработка
    public void processShapes(List<Drawable> shapes) {
        for (Drawable shape : shapes) {
            shape.draw();
            System.out.println("Тип: " + shape.getType());
            System.out.println("Площадь: " + shape.getArea());
            System.out.println();
        }
    }
    
    public void moveShapes(List<Movable> movables, double dx, double dy) {
        for (Movable movable : movables) {
            movable.move(dx, dy);
        }
    }
    
    // Демонстрация
    public void demonstrateInterfacePolymorphism() {
        List<Drawable> shapes = new ArrayList<>();
        List<Movable> movables = new ArrayList<>();
        
        Circle circle = new Circle(2.0, 0, 0);
        Rectangle rectangle = new Rectangle(3.0, 4.0, 5, 5);
        
        shapes.add(circle);
        shapes.add(rectangle);
        movables.add(circle);
        movables.add(rectangle);
        
        System.out.println("=== Formen zeichnen ===");
        processShapes(shapes);
        
        System.out.println("=== Formen verschieben ===");
        moveShapes(movables, 10, 10);
        
        System.out.println("=== Nach dem Verschieben ===");
        processShapes(shapes);
    }
}

UML-нотация для полиморфизма

Соглашения диаграмм классов

@startuml
' Полиморфное отношение в UML
abstract class PaymentProcessor {
    +processPayment(amount: double): boolean {abstract}
    +validatePayment(amount: double): boolean {abstract}
}

class CreditCardProcessor {
    +processPayment(amount: double): boolean
    +validatePayment(amount: double): boolean
    +validateCardNumber(number: String): boolean
}

class PayPalProcessor {
    +processPayment(amount: double): boolean
    +validatePayment(amount: double): boolean
    +validateEmail(email: String): boolean
}

class BankTransferProcessor {
    +processPayment(amount: double): boolean
    +validatePayment(amount: double): boolean
    +validateBankDetails(iban: String): boolean
}

PaymentProcessor <|-- CreditCardProcessor
PaymentProcessor <|-- PayPalProcessor
PaymentProcessor <|-- BankTransferProcessor

' Interface для полиморфных операций
interface Refundable {
    +processRefund(amount: double): boolean
    +getRefundStatus(): String
}

CreditCardProcessor ..|> Refundable
PayPalProcessor ..|> Refundable
BankTransferProcessor ..|> Refundable

@enduml

Диаграмма последовательности для полиморфных вызовов

@startuml
actor Customer
Customer -> PaymentSystem: makePayment(amount, method)
activate PaymentSystem

PaymentSystem -> PaymentProcessorFactory: createProcessor(method)
activate PaymentProcessorFactory
PaymentProcessorFactory --> PaymentSystem: processor
deactivate PaymentProcessorFactory

PaymentSystem -> PaymentProcessor: processPayment(amount)
activate PaymentProcessor

alt Credit Card
    PaymentProcessor -> CreditCardProcessor: processPayment(amount)
    CreditCardProcessor --> PaymentProcessor: success
else PayPal
    PaymentProcessor -> PayPalProcessor: processPayment(amount)
    PayPalProcessor --> PaymentProcessor: success
else Bank Transfer
    PaymentProcessor -> BankTransferProcessor: processPayment(amount)
    BankTransferProcessor --> PaymentProcessor: success
end

PaymentProcessor --> PaymentSystem: result
deactivate PaymentProcessor

PaymentSystem --> Customer: payment result
deactivate PaymentSystem
@enduml

Лучшие практики полиморфизма

1. Принцип подстановки Лисков

// Хорошо: Rectangle может заменить Shape везде
public class GoodPolymorphism {
    
    public interface Shape {
        double area();
        double perimeter();
        void move(double dx, double dy);
    }
    
    public class Rectangle implements Shape {
        private double width, height, x, y;
        
        public Rectangle(double width, double height, double x, double y) {
            this.width = width;
            this.height = height;
            this.x = x;
            this.y = y;
        }
        
        @Override
        public double area() {
            return width * height;
        }
        
        @Override
        public double perimeter() {
            return 2 * (width + height);
        }
        
        @Override
        public void move(double dx, double dy) {
            x += dx;
            y += dy;
        }
        
        // Дополнительные методы не нарушают LSP
        public double getWidth() { return width; }
        public double getHeight() { return height; }
    }
    
    public class Square implements Shape {
        private double side, x, y;
        
        public Square(double side, double x, double y) {
            this.side = side;
            this.x = x;
            this.y = y;
        }
        
        @Override
        public double area() {
            return side * side;
        }
        
        @Override
        public double perimeter() {
            return 4 * side;
        }
        
        @Override
        public void move(double dx, double dy) {
            x += dx;
            y += dy;
        }
        
        public double getSide() { return side; }
    }
}

2. Разделение интерфейса

// Плохо: Слишком большой интерфейс
public interface BadShape {
    double area();
    double perimeter();
    void move(double dx, double dy);
    void rotate(double angle);
    void resize(double factor);
    Color getColor();
    void setColor(Color color);
}

// Хорошо: Специализированные интерфейсы
public interface Drawable {
    void draw(Graphics g);
}

public interface Movable {
    void move(double dx, double dy);
    void setPosition(double x, double y);
    double[] getPosition();
}

public interface Resizable {
    void resize(double factor);
    void setSize(double width, double height);
}

public interface Rotatable {
    void rotate(double angle);
    double getRotation();
}

public interface Colored {
    Color getColor();
    void setColor(Color color);
}

// Класс реализует только нужные интерфейсы
public class Circle implements Drawable, Movable, Resizable, Colored {
    // Реализация...
}

3. Шаблонный метод

public abstract class DataProcessor {
    
    // Шаблонный метод - определяет алгоритм
    public final void processData() {
        loadData();
        if (validateData()) {
            transformData();
            saveData();
            onSuccess();
        } else {
            onError();
        }
        cleanup();
    }
    
    // Абстрактные методы - должны быть реализованы
    protected abstract void loadData();
    protected abstract boolean validateData();
    protected abstract void transformData();
    protected abstract void saveData();
    
    // Методы-перехватчики - могут быть переопределены
    protected void onSuccess() {
        System.out.println("Обработка успешна");
    }
    
    protected void onError() {
        System.out.println("Обработка не удалась");
    }
    
    protected void cleanup() {
        System.out.println("Очистка ресурсов");
    }
}

public class CSVProcessor extends DataProcessor {
    @Override
    protected void loadData() {
        System.out.println("Загружаю CSV данные");
    }
    
    @Override
    protected boolean validateData() {
        System.out.println("Проверяю CSV данные");
        return true;
    }
    
    @Override
    protected void transformData() {
        System.out.println("Трансформирую CSV данные");
    }
    
    @Override
    protected void saveData() {
        System.out.println("Сохраняю CSV данные");
    }
    
    @Override
    protected void onSuccess() {
        System.out.println("CSV успешно обработан!");
    }
}

Концепции для экзамена

Важные различия

  1. Переопределение vs Перегрузка

    • Переопределение: одинаковая сигнатура в подклассе
    • Перегрузка: одинаковое имя, разные параметры
  2. Статическое vs Динамическое связывание

    • Статическое: перегрузка (на этапе компиляции)
    • Динамическое: переопределение (во время выполнения)
  3. Абстрактный класс vs Интерфейс

    • Абстрактный класс: общая реализация
    • Интерфейс: чистый контракт
  4. Generics vs Наследование

    • Generics: типобезопасность на этапе компиляции
    • Наследование: полиморфизм во время выполнения

Типичные задачи на экзамене

  1. Рисуйте UML диаграммы для полиморфных отношений
  2. Реализуйте переопределённые методы
  3. Объясните динамическое связывание
  4. Сравните различные типы полиморфизма
  5. Проектируйте полиморфные иерархии классов

Итог

Полиморфизм - мощный инструмент для создания гибкой архитектуры:

  • Переопределение обеспечивает динамическое связывание и полиморфизм во время выполнения
  • Перегрузка даёт статическое связывание и полиморфизм на этапе компиляции
  • Generics позволяют безопасно переиспользовать код
  • Интерфейсы определяют полиморфные контракты

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

Продолжение в пути обучения UML

Следующая статья в пути обучения UML посвящена UML диаграммам классов: отношения - Association, Aggregation, Composition, Dependency, Inheritance и детально рассматривает различные типы отношений в диаграммах классов.

Назад к блогу
Share:

Похожие статьи