Повторне використання коду в класах. Поняття композиції, спадковості, делегування. Ключове слово extends. Приклади

Повторне використання коду в класах. Поняття композиції, спадковості, делегування. Ключове слово extends. Приклади


Зміст


1. Яким чином повторне використання коду застосовується у класах?

Суть повторного використання коду полягає в тому, що не потрібно писати програмний код ще раз. Набагато ефективніше використовувати раніше створений і перевірений (протестований) програмний код. Для цього найкраще підходять класи, які увібрали в себе усі переваги об’єктно-орієнтованого підходу до програмування.

До появи об’єктно-орієнтованого програмування існувало процедурно-орієнтоване програмування. В процедурно-орієнтованому підході для того, щоб використати повторно програмний код, використовувався метод копіювання цього коду. Такий підхід не був достатньо ефективним і, при збільшенні коду програмної системи, викликав стрімке зростання помилок.

2. Які існують способи (підходи) побудови класів для ефективного забезпечення повторного використання коду?

Якщо потрібно повторно використати існуючий програмний код, то класи мають будуватись так, щоб не змінити (пошкодити) цей код.

В об’єктно-орієнтованих мовах програмування (Java, C++, C# та інші) існують три способи побудови класів, що повторно використовують програмний код:

  • композиція (composition). У цьому випадку клас містить у собі об’єкт іншого класу, програмний код якого потрібно використати. Такий підхід використовує функціональність готового коду;
  • успадкування (inheritance). Клас успадковує інший код (класу) і змінює його властивості (поведінку) або додає власні можливості (поведінку) до успадкованого коду;
  • делегування. Це щось проміжне між композицією та успадкуванням.

Ці способи ще називають відношеннями між класами. В усіх способах будуються нові класи (типи) на базі вже існуючих класів (типів).



3. Приклади композиції в класах

Приклад 1. Клас B включає в себе об’єкти (екземпляри) класу A. Значення об’єктів класу A ініціалізуються різними способами:

  • безпосередньо (об’єкт a1);
  • в конструкторі (об’єкт a2).
// Демонстрація композиції у класі
class A
{
    int a; // внутрішня змінна класу A
    
    // конструктор
    A() { a = 1; }
    
    // методи класу A
    void SetA(int a) { this.a = a; }  
    int GetA() { return a; }    
}

public class B
{
    int b; // внутрішня змінна класу B
    
    // композиція: у класі B оголошуються об'єкти класу A
    A a1 = new A();
    A a2;
    
    B()
    {
        b = 0;
        a2 = new A();
        a2.SetA(15);
    }
  
    // методи доступу до внутрішньої змінної b
    void SetB(int nb) { b = nb; }
    int GetB() { return b; }
    
    public static void main(String[] args)
    {
        // композиція: у класі B оголошуються об'єкти класу A
        B objB = new B(); // об'єкт (екземпляр) класу B
         
        // демонстрація доступу
        int t;
          
        // доступ до об'єкту a2 класу A, в якому реалізований метод GetA()
        t = objB.a2.GetA(); // t = 15
         
        // доступ до об'єкту a1 класу A
        t = objB.a1.GetA(); // t = 1
         
        System.out.println(t);
    }
}

Приклад 2. Клас Pixel містить об’єкт класу Point.

// Клас Point, що описує точку на площині
class Point
{
    int x, y; // внутрішні змінні
   
    // метод доступу
    void SetXY(int nx, int ny)
    {
        x = nx; y = ny;
    }
    
    int GetX() { return x; }
    int GetY() { return y; }    
}


// Клас Pixel, що містить в собі об'єкт класу Point
// Демонстрація композиції
public class Pixel
{
    int color;
    Point p; // об'єкт класу Point - композиція
   
    // конструктор
    Pixel()
    {
        color = 0;
        p = new Point(); // виділити пам'ять для об'єкту p - обов'язково
    }

    int GetColor() { return color; }  
    void SetXYColor(int nx, int ny, int ncolor)
    {
        // у класі Pixel формуються значення об'єкту класу Point
        p.SetXY(nx, ny); // виклик методу об'єкту p класу Point
        color = ncolor;
    }    

    public static void main(String[] args)
    {
        // демонстрація використання композиції
        Pixel px = new Pixel();        
        px.SetXYColor(4, 7, 5);
          
        // перевірка
        // доступ до методів класу Point через об'єкт p, що оголошений в класі Pixel
        int d = px.p.GetX(); // d = 4
        d = px.p.GetY(); // d = 7
          
        px.p.SetXY(12, 23);
        d = px.p.GetX();
          
        // виклик методу класу Pixel
        d = px.GetColor(); // d = 5       
          
        System.out.println("d = " + d);     
      }
}

4. Яка загальна форма найпростішого успадкування класом іншого класу. Ключове слово extends

Якщо в класі B потрібно успадкувати програмний код класу A, то найпростіша загальна форма такого оголошення наступна:

class A
{
    // тіло класу A
}

class B extends A
{
    // тіло класу B
}

У цьому випадку методи і внутрішні змінні класу A є доступні в класі B. Тут є одне обмеження: доступними в класі B є тільки методи та змінні, що не містять в оголошенні модифікатор доступу private (це вже інша тема).

5. Приклади спадковості в класах

Приклад 1. Приклад успадкування класу A класом B. У класі B успадковуються:

  • внутрішня змінна a класу A;
  • внутрішній метод SetA() класу A.

У методі Demo() класу B демонструється доступ до змінної та методу класу A.

// базовий клас
class A
{
    int a; // внутрішня змінна класу A

    // внутрішній метод класу A
    void SetA(int na)
    {
        a = na;
    }
}

// клас B успадковує поля та методи класу A
public class B extends A
{
    int b; // внутрішня змінна класу B

    // внутрішній метод класу B
    void SetB(int nb)
    {
        b = nb;
    }

    // демонстраційний метод класу B
    void Demo()
    {
        // доступ до полів та методів класу A
        a = 23;
        SetA(93);

        // доступ до полів та методів класу B
        b = 90;
        SetB(50);
    }
}

Приклад 2. Дано клас Point, що описує точку на площині. Клас Point є базовим для класу Pixel. Точніше клас Pixel розширює можливості класу Point шляхом додавання додаткової змінної color та методів SetColor(), SetXYColor().

class Point
{
    int x, y;

    void SetXY(int nx, int ny)
    {
        x = nx; y = ny;
    }
}

public class Pixel extends Point // клас Pixel успадковує клас Point
{
    int color;

    void SetColor(int ncolor)
    {
        color = ncolor;
    }

    void SetXYColor(int nx, int ny, int ncolor)
    {
        // у класі Pixel доступні внутрішні змінні та методи класу Point
        x = nx; y = ny; color = ncolor;
    }

    public static void main(String[] args)
    {
        // демонстрація використання спадковості
        Pixel p1 = new Pixel();
        p1.SetXY(4, 6); // доступ до методу класу Point з об'єкту класу Pixel

        // перевірка
        int d = p1.x; // доступ до внутрішньої змінної класу Point через об'єкт класу Pixel
    }
}

У функції main() класу Pixel створюється об’єкт (екземпляр) класу Pixel. В екземплярі відбувається доступ до змінної x, яка оголошується в класі Point. Такий код є коректний, тому що клас Pixel успадковує клас Point. Це є особливістю (перевагою) спадковості, при якій можна розширювати можливості вже існуючого класу. Клас Pixel є надмножиною над класом Point.

6. Приклад поєднання композиції і спадковості

На прикладі оголошення трьох класів демонструється поєднання композиції та спадковості.

Оголошується три класи з іменами Point, Line, LineColor. Між класами Point та Line реалізовано композицію. Клас Line містить оголошення об’єктів класу Point.

Між класами LineColor та Line реалізовано спадковість. Клас LineColor успадковує внутрішні змінні та методи класу Line.

Схема поєднання зображена на рисунку.

Java. Схема поєднання композиції та спадковості для трьох класів

Рисунок. Схема поєднання композиції та спадковості для трьох класів

Вихідний код модуля з розробленими класами.

// поєднання композиції і спадковості
// клас, що описує точку на площині
class Point
{
    int x, y;

    void SetXY(int nx, int ny)
    {
        x = nx;
        y = ny;
    }

    int GetX() { return x; }
    int GetY() { return y; }
}

// лінія, що складається з двох точок
class Line
{
    // композиція
    Point p1;
    Point p2;

    // конструктор класу Line
    Line()
    {
        p1 = new Point();
        p2 = new Point();
        p1.SetXY(0, 0);
        p2.SetXY(1, 1);
    }

    // методи доступу
    Point GetP1() { return p1; }
    Point GetP2() { return p2; }

    void SetPoints(Point np1, Point np2)
    {
        p1.SetXY(np1.GetX(), np1.GetY());
        p2.SetXY(np2.GetX(), np2.GetY());
    }
}

// клас, що розширює клас Line (додає колір)
public class LineColor extends Line
{
    int color; // додається внутрішня змінна color - колір лінії

    LineColor() // конструктор класу LineColor()
    {
        color = 1;
    }

    // метод Length() класу LineColor використовує методи класу Line
    // довжина лінії
    double Length()
    {
        double len;
        int x1, y1, x2, y2;

        // метод Length має доступ до p1 та p2 класу Line
        x1 = p1.GetX(); y1 = p1.GetY();
        x2 = p2.GetX(); y2 = p2.GetY();

        len = Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
 
        return len;
    }

    public static void main(String[] args)
    {
        // демонстрація поєднання композиції та спадковості
        LineColor lc = new LineColor(); // екземпляр класу LineColor
        Point pt1, pt2; // додаткові змінні
        double length;

        pt1 = new Point();
        pt2 = new Point();
        pt1.SetXY(3, 4);
        pt2.SetXY(5, 7);

        lc.SetPoints(pt1, pt2);
        length = lc.Length(); // length = 3.605551275463989

        System.out.println("Length = " + length);
    }
}

7. Яка особливість делегування у порівнянні зі спадковістю та композицією? Приклад

Делегування є проміжною ланкою між спадковістю та композицією. Наступний приклад демонструє суть делегування.

class A
{
    // метод класу A
    void MethodA() { ... }

    // ...
}

// клас B робить делегування
class B
{
    private A objA = new A();

    // делегований метод
    MethodA()
    {
        objA.MethodA(); // переадресація вбудованому об'єкту objA
    }
}

При делегуванні клас A є включений в клас B як у випадку з композицією. Тобто клас B містить об’єкт класу A. Але доступ до методів класу A відбувається так якби клас B був успадкований від класу A. Це здійснюється шляхом перевизначення методу MethodA() класу A у класі B:

MethodA()
{
    objA.MethodA();
}

Після такого перевизначення звертання до методу MethodA() класу B буде виглядати так якби MethodA() був успадкований:

...

// оголосити об’єкт класу B
B objB = new B();

// звертання до MethodA() класу B, а виглядає як до методу MethodA() класу A
B.MethodA(); // викликається B.objA.MethodA()

...

Така організація відношення між класами називається делегуванням. Метод MethodA() класу B називається делегованим методом.

8. Яка відмінність між композицією та спадковістю?

І композиція, і спадковість розширюють можливості класу. І композиція, і спадковість дозволяють розміщувати підоб’єкти класу всередині новостворюваного класу.

Однак, між композицією і спадковістю є відмінності з точки зору вибору способу створення нового класу.

Спадковість дозволяє розширювати можливості нового класу з точки зору його спеціалізації. Це означає, що створюється спеціалізована версія вже існуючого класу. Тобто, береться вже існуючий клас (базовий клас) і створюється його нова спеціалізована версія. Як правило, в ролі базового класу виступає деякий клас загального призначення, в якому виділені загальні риси задачі. Спадковістю виражається взаємозв’язок “є” або “являється”.

Композиція використовується, коли потрібно використати функціональність вже існуючого класу в іншому (новоствореному) класі. У цьому випадку в новоствореному класі оголошується об’єкт класу, функціональність якого потрібно використати.

Об’єкт класу в іншому класі може бути прихованим (private), відкритим (public) чи захищеним (protected).


Зв’язані теми