You are on page 1of 219

р су

і ку дуже небагато поширених


ач повноцінних мов програмування
з ад залишаються сьогодні
ь та без підтримки ОО-стилю
ніст
ь
ту ал
Ак
Задачі курсу:

1 опанувати основні принципи


написання програм в ОО-стилі

2 бути обізнаними про відмінності


в реалізації ОО-принципів
в різних мовах програмування

3 навчитися на практиці розробляти


ПЗ в ОО-стилі та ...
(бонус на курсову роботу)
... з графічним інтерфейсом користувача
Ум
о вно
рек
Передумови для вивчення курсу: Література для вивчення курсу: ом
ен
до
. н
ва
1 знання принципів процедурного та Принципи ООП
Г. Буч. Объектно-ориентированный анализ
і ав
и проектирование с примерами приложений на С++ то ри
структурного програмування
Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. (“Gang of Four”).
Патерни проектування
2 знання мови програмування C (GoF Patterns)
Приемы объектно-ориентированного проектирования. Паттерны
проектирования

С. Прата, Х.М. Дейтел, Г. Шилдт, Р. Лафоре, Д. Ліберті, Б. Еккель,


3 вміння складати алгоритми С++ / STL
Б. Страуструп, С. Майерс, Г. Саттер, А. Александреску
вирішення обчислювальних задач.
Java Г. Шилдт, Б. Еккель, Дж. Блох, Р. Мартін, К.Хорстман
Розуміння суті алгоритмічної
декомпозиції задач.
C# / .Net Д. Скіт, Б. Вагнер, Дж. Ріхтер, Е.Троелсен, Г.Шилдт

4 навички роботи зі структурами


даних Python М. Лутц, М. Доусон, М. Саммерфілд, Д. Бейдер

5 знання принципів побудови


JavaScript / TypeScript* П. Уілтон, Д. Фленеган, М. Моррісон, Д. Крокфорд, К. Сімпсон, ...*
динамічних структур даних
Першою мовою програмування, в якій були запропоновані принципи об'єктної орієнтованості, була Симула. На
момент своєї появи (в 1967 році), ця мова програмування запропонувала революційні ідеї: об'єкти, класи, віртуальні
методи тощо. Більшість концепцій були розвинені Аланом Кеєм та Деном Інгаллсом у мові Smalltalk. Саме вона
стала першою широко поширеною об'єктно - орієнтованою мовою програмування (wiki)

Lisp

perl

C#

Python 3
ОО-декомпозиція
Одним з базових способiв керування складними системами є декомпозицiя, тобто iтерацiйне або рекурсивне
роздiлення системи на простiшi пiдсистеми, що пов’язанi мiж собою: “розділяй та володарюй”

Структурне програмування дотримується засад алгоритмiчної декомпозицiї: розбиття складної задачi (алгоритму) на
бiльш простi та дрiбнi алгоритми, органiзованi в загальну структуру за допомогою конструкцiй послiдовного, циклiчного
та умовного виконання

Об’єктно-орієнтоване програмування дотримується засад об’єктної декомпозицiї: розбиття складної системи на


складовi елементи (об’єкти) за їх приналежнiстю до рiзних абстракцiй предметної областi. Кожний з елементiв
характеризується своєю власною поведiнкою, а поведiнка системи забезпечується взаємодiєю цих автономних
елементiв.

Поява та розвиток ОО-парадигми обумовленi обмеженнями iнших бiльш раннiх парадигм програмування. Процедурнi
мови програмування (FORTRAN, Pascal, C) вирішують проблему складності ПЗ поєднанням модульної, процедурної
та структурної парадигм, що дає багаторiвневу (iєрархiчну) декомпозицiю: розбиття програми на окремi модулi,
модулi — на процедури, процедури — на структурнi блоки.

Однак при подальшому зростаннi складностi ПЗ проявляються недолiки структурного програмування:

- слабкий (відсутній) контроль доступу процедур до глобальних даних


- роздiлення процедур та даних погано вiдбиває структуру реального свiту. Реальнi сутностi не є або даними, або
процедурами, а є сукупностями властивостей (описується даними) та поведiнки (описується процедурами)

Основною iдеєю ОО-пiдходу є об’єднання даних та дiй над цими даними у єдине цiле пiд назвою об’єкт
ОО-парадигма

Об’єктно-орiєнтоване програмування — це
парадигма програмування, що заснована на
представленнi програми у виглядi сукупностi
взаємодiючих об’єктiв («об’єктна програмна модель»),
кожен з яких є екземпляром певного класу, а класи
формують певну iєрархiю відношень та наслiдування
властивостей.

ООП передбачає об’єднання усiх властивостей


абстракцiї об’єкта в єдину одиницю — клас. Клас —
абстрактний тип даних, що описує будову об’єкта та
його поведiнку, а об’єкт є екземпляром свого класу,
тобто створеною під час виконання програми сутністю,
будова (дані та поведінка) якої описується класом.

Клас є складеним типом даних,


що мiстить такі члени:
- поля даних — данi, що
задають параметри конкретного
об’єкта,
- та методи — процедури,
пов’язанi з даним класом
(функцiї-члени).
#include <iostream> #include <iostream>
#include <stdio.h>
#include <cmath> #include <cmath>
#include <math.h>

struct Vector struct Vector


struct Vector
{ {
{
double x, y; double x, y;
double x, y;
double size() Vector(double x, double y)
};
{ {
return sqrt(x * x + y * y); this->x = x;
double size(struct Vector v)
} this->y = y;
{
}; }
return sqrt(v.x * v.x + v.y * v.y);
double size()
}
{
int main() return sqrt(x * x + y * y);
void main()
{ }
{
Vector v1; };
struct Vector v1;
v1.x = 3; v1.x = 3;
v1.y = 4; int main()
v1.y = 4;
std::cout << "size: " << v1.size(); {
printf("size: %f", size(v1));
} Vector v1(3, 4);
}
std::cout << "size: " << v1.size();
}

Методи умовно поділяють на:


модифiкатори — методи, що змiнюють внутрiшнiй стан (данi) об’єкта
селектори — методи, що лише зчитують, але не змiнюють стан об’єкта
конструктори — процедури iнiцiалiзацiї об’єкта при його створеннi
деструктори — процедури, що вивiльняють ресурси при знищеннi об’єкта
ОО принципи
Основні принципи ОО-парадигми: абстрагування, інкапсуляція, наслідування, поліморфізм

Абстрагування (метод вирiшення складних задач) — процес видiлення абстракцiй у предметнiй областi.

Абстракцiя — сукупнiсть суттєвих характеристик об’єкта, що вiдрiзняють його вiд усiх iнших видiв об’єктiв, таким
чином чiтко встановлюючи його концептуальнi межi.

ООП передбачає об’єднання усiх властивостей абстракцiї об’єкта в єдину одиницю “клас” — абстрактний тип даних,
що описує будову об’єкта та його поведiнку.

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

Iнтерфейс — це набiр операцiй, що характеризує зовнiшню поведнiку об’єкта. Внутрiшня реалiзацiя визначає
механiзми для забезпечення цiєї поведiнки.

Iнкапсуляцiя — це механiзм вiдокремлення елементiв об’єкта, що визначають його будову та поведiнку. Мета
iнкапсуляцiї — iзолювати абстракцiю поведiнки (iнтерфейс) об’єктiв класу вiд її реалзiацiї.

Складовою цього механiзму є обмеження доступу до внутрiшньої будови (даних) об’єкта, приховування усiх
внутрiшнiх деталей, що не впливають на зовнiшню поведiнку.

Важливе завдання інкапсуляції - зберегти стан об'єкту (тобто множину значень усіх полів) несуперечливим при дії
на об'єкт через його інтерфейс. Рівень
абстрагування
повинен відповідати
вирішуваному
завданнюЯк досягти суперечливого стану:
змінили одне поле,
але не обчислили повторно
решту залежних полів
void set(double x, double y) int main()
#include <iostream>
{ {
#include <cmath>
this->x = x; Vector v1;
this->y = y; // std::cout << "x: " << v1.x
class Vector
sz = calcSize(x, y); << ", y:" << v1.y
{
} << std::endl;
public:
std::cout << "x: " << v1.get_x()
Vector(double x = 0, double y = 0)
private: << ", y:" << v1.get_y()
: x(x), y(y), sz(calcSize(x, y))
double x, y, sz; << std::endl;
{}
double calcSize(double x, double y) std::cout << "size: " << v1.size()
double getX() const
{ << std::endl;
{
return x; return sqrt(x * x + y * y);
} v1.set(3,4);
}
}; std::cout << "x: " << v1.get_x()
double getY() const
<< ", y:" << v1.get_y()
{
<< std::endl;
return y; ідея: не треба обчислювати повторно std::cout << "size: " << v1.size()
} довжину, якщо не змінювались координати. << std::endl;
double size() const Але тепер слід убезпечити координати та довжину }
{ від “розсинхронізації” у випадку зміни координат
return sz;
}
Рекомендація: не переобтяжуйте
Типи доступу:
+ public (відкритий): доступ з будь-якої частини програми
прості класи (структури) зайвими
# protected (захищений): доступ з методів цього класу та його наслідників “гетерами-сетерами”, якщо не
- private (закритий): доступ лише з методів цього ж класу треба забезпечувати
несуперечливість стану об'єкта
інкапсуляція ефективно обмежує коло пошуку помилок

void readRatings ()
void addVote ()
struct Rating void deleteRating () {
{ r.mark = r.sum + new_mark;
int id; r.mark = r.mark / (++votes);
int votes; void removeVote () }
double sum;
double mark;
} r;
void findRatings ()
{…
void analyzeRatings () if (r.id = id)

void sortRatings () else filterRatings();
void printRatings () }

void saveRatings ()
void filterRatings () і -коду
задача - лічильник оцінок: { ... о с пагет
р но г
if (r.votes = criteria) ду
у пр оце
мент
...
} ф раг
л ад
прик
Java та C# - “чисті” ОО-мови, без функцій поза межами класів

public class Rating { public class Main {


public void AddVote (double newmark) { public static void main(String[] args) {
private int id; sum += newmark;
private int votes; mark = sum / (++votes); Rating data = new Rating(1);
private double mark; } System.out.println("mark is "
private double sum; + data.GetMark() + " of "
public void RemoveVote (double oldmark) { + data.GetVotes() + " votes");
public Rating(int newid) { if (--votes > 0) {
id = newid; sum -= oldmark; data.AddVote(4.5);
} mark = sum / votes; data.AddVote(4);
} data.AddVote(5);
public int GetVotes() { else System.out.println("mark is "
return votes; { + data.GetMark() + " of "
} sum = mark = votes = 0; + data.GetVotes() + " votes");
}
public double GetMark() { } data.RemoveVote(4);
return mark; } System.out.println("mark is "
} + data.GetMark() + " of "
+ data.GetVotes() + " votes");
mark is 0.0 of 0 votes
mark is 4.5 of 3 votes data.RemoveVote(5);
mark is 4.75 of 2 votes data.RemoveVote(4.5);
System.out.println("mark is "
mark is 0.0 of 0 votes + data.GetMark() + " of "
+ data.GetVotes() + " votes");
ідея: закрити всі поля, що є взаємозалежними та визначають деталі обліку оцінок, }
натомість надати відкритий інтерфейс з мінімально необхідною для вирішення задачі }
кількістю методів
using System;
class Program
public double I
class Resistance {
{
{ static void Main(string[] args)
get
private double i, u; {
{
private readonly double r; Resistance resistor = new Resistance(2);
return this.i;
resistor.I = 10;
}
public Resistance(double r) Console.WriteLine("I = {0}, U = {1}",
set
{ resistor.I, resistor.U);
{
this.r = r; this.i = value;
} resistor.U++;
this.u = i * r;
Console.WriteLine("I = {0}, U = {1}",
}
resistor.I, resistor.U);
}
}
властивість (property): public double U
}
синтаксична конструкція, {
що поєднує 2 методи - get
селектор та модифікатор - { I = 10, U = 20
для конкретного поля return this.u;
}
I = 10,5, U = 21
set
{
this.u = value;
this.i = u / r;
}
}
}
Типiзацiя — обмеження, що перешкоджає
(випадковій) взаємозамiнюваностi об’єктів
class ComplexNumber:
рiзних типiв.
def __init__(self, r=0, i=0): конструктор
self.real = r
self.imag = i Буває жостка (C++, Java) чи слабка (Python)

# pseudo-private field
self.__private = 0

def get_data(self):
print(f'{self.real}+{self.imag}j')

num1 = ComplexNumber(2, 3)
num1.get_data()
num2 = ComplexNumber(5)
num2.attr = 10 динамічне створення атрибутів (полів)
print((num2.real, num2.imag,
num2.attr))
# print(num1.attr)
# print(num1.__private)
print(num1._ComplexNumber__private)

2+3j
(5, 0, 10)
0
function Person(name) { JavaScript - прототипно-орієнтована мова
this.name = name;
}

Person.prototype.sayHi = function() {
alert(this.name);
};

let bob = new Person("Bob");


TypeScript розширює JS,
bob.sayHi(); підтримуючи статичну типізацію
та “традиційне” для ООП використання
класів

ES6
class Person {
class Person{ name: string;
gender: string = 'Male';
constructor(name) { private id: number;
this.name = name;
} constructor (name: string, id: number, gender?: string) {
this.name = name;
sayHi() { this.id = id;
alert(this.name);
} if (gender) this.gender = gender;
} }
}
let bob = new Person("Bob");
bob.sayHi();
UML (англ. Unified Modeling Language) —
уніфікована мова моделювання, використовується
у парадигмі об'єктно-орієнтованого програмування.
Є невід'ємною частиною уніфікованого процесу
розробки програмного забезпечення. UML ...
використовує графічні позначення для створення
абстрактної моделі системи, яка називається UML-
моделлю. UML був створений для визначення,
візуалізації, проектування й документування, в
основному, програмних систем. UML не є мовою
програмування, але в засобах виконання UML-
моделей як інтерпретованого коду можлива
кодогенерація (wiki)

https://app.diagrams.net/

http://www.umlet.com/umletino/umletino.html
...вона ж Діаграма варіантів використання
Iєрархiя — багаторiвнева упорядкована система абстракцiй.
В ООП розрiзняють iєрархiчнi вiдношення типу:

“X – частина вiд Y” або “Y включає X” (асоцiативнi вiдношення агрегацiї або композицiї)

“X узагальнює Y” або “Y конкретизує (наслiдує) X” (вiдношення узагальнення або наслiдування).

X — базовий клас (батькiвський, суперклас),


Y — похiдний клас (нащадок, пiдклас)
При цьому завжди тип “Y” може розглядатися і як тип “X”, але не навпаки
https://www.visual-paradigm.com/guide/uml-unified-modeling-language/what-is-object-diagram/
https://circle.visual-paradigm.com/sequence-diagram-example/
https://www.researchgate.net/figure/UML-state-machine-diagram-showing-a-user-interface-navigation-map-for-Student-
actor_fig6_228988537
VCS
Централізовані системи контролю версій Розподілені системи контролю версій
Централізована система контролю версій (клієнт-серверна) — Розподілена система контролю версій (англ. Distributed Version
система, дані в якій зберігаються в єдиному «серверному» Control System, DVCS) — система, яка використовує замість моделі
сховищі. Весь обмін файлами відбувається з використанням клієнт-сервер, розподілену модель зберігання файлів. Така система
центрального сервера. Є можливість створення та роботи з не потребує сервера, адже всі файли знаходяться на кожному з
локальними репозиторіями (робочими копіями). комп'ютерів.
Переваги: Переваги:
● Загальна нумерація версій; ● Кожний з розробників працює зі своїм власним репозиторієм;
● Дані знаходяться на одному сервері; ● Рішення щодо злиття гілок приймається керівником проекту;
● Можлива реалізація функції блокування файлів; ● Немає потреби в мережевому з'єднанні;
● Можливість керування доступом до файлів; Недоліки:
Недоліки: ● Немає можливості контролю доступу до файлів;
● Потреба в мережевому з'єднанні для оновлення робочої ● Відсутня загальна нумерація версії файла;
копії чи збереження змін; ● Значно більша кількість необхідного дискового простору;
До таких систем відносять Subversion, Team Foundation Server. ● Немає можливості блокування файлів;
До таких систем відносять Git, Mercurial, SVK, Monotone, Codeville,
BitKeeper
https://guides.github.com/activities/hello-world/

https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository

створити клон репозиторію git clone https://github.com/YOUR-USERNAME/YOUR-REPO


витягнути останню версію з віддаленого репозиторію git pull
створити нову гілку git checkout -b [branch name]
показати змінені файли git status
показати зміни у файлах git diff
додати файл у коміт git add [file]
зберегти коміт git commit -m "[commit message]"
виштовхнути всі зміни у віддалений репозиторій git push
журнал git log
компіляція, конструктори, вказівники та посилання,
перевантаження методів, композиція та агрегація,
наслідування, таблиця віртуальних методів, поліморфізм,
множинне наслідування
Один вихідний код → різні компілятори під різні платформи → різні виконувані файли під різні платформи:

g++ hello.cpp ./a.out (Linux, GCC)


cl.exe hello.cpp hello.exe (Windows, MS VS)
Один вихідний код → один скомпільований байт-код → різні віртуальні машини для виконання байт-коду
на різних платформах:

javac HelloWorld.java
java -classpath . HelloWorld
managed-код
С++

компілятор C++

Один вихідний код → один скомпільований проміжний код (MSIL) → JIT компілятор (під час виконання)→
виконуваний код для конкретної платформи (Windows):

cl file1.cpp file2.cpp file3.cpp /out:program1.exe


інтерфейс + реалізація + споживач (клієнт)

Drawing.h Drawing.cpp main.cpp


спосіб уникнути
багаторазового
включення вмісту h-файлів #i ncl ude <i ost r eam> #i ncl ude " Dr awi ng. h"
(альтернатива: #i ncl ude " Dr awi ng. h"
#pragma once) i nt mai n( )
Dr awi ng: : Dr awi ng( ) {
{ Dr awi ng dr awi ng;
st d: : cout << " Dr awi ng cr eat ed" dr awi ng. Cl ear ( ) ;
<< st d: : endl ; }
#i f ndef DRAWI NG_H
}
#def i ne DRAWI NG_H
Dr awi ng cr eat ed
Dr awi ng: : ~Dr awi ng( ) Dr awi ng cl ear ed
cl ass Dr awi ng Dr awi ng dest r oyed
{
{
st d: : cout << " Dr awi ng dest r oyed"
publ i c:
<< st d: : endl ;
Dr awi ng( ) ;
}
~Dr awi ng( ) ; к оду
+
voi d Cl ear ( ) ; я C+
voi d Dr awi ng: : Cl ear ( ) н
}; рмлен
{ о
оф
st d: : cout << " Dr awi ng cl ear ed" оване
#endi f / / DRAWI NG_H
<< st d: : endl ; оменд
рек
}
конструктор за замовчанням (default ctor):
- той, що не приймає аргументів
(або є версія, що допускає виклик без
аругментів, тобто задані значення аргументів
за замовчанням)
- та / або: той, що створюється компілятором
C++ автоматично
class Person
{
public:
Person (); // за замовчанням
Person (std::string name); // з параметром
Person (const Person& orig); // copy-ctor (автогенерація за відсутності)
~Person (); // деструктор
};

Java
class Person {
public Person (); // без параметрів (аргументів)
public Person (String name); // з параметром
public Person (Person orig); // copy-ctor
}
Сигнатура методу - назва методу класу та перелік типів його параметрів

mulitply (int, int)

multiply (float, float)

concat (std::string, std::string)

concat (std::string) // concat with current value

перевантаження методу: різні набори параметрів при однаковому імені


методу (незалежно від типу значення, що повертається) = різні сигнатури
методу з однаковим іменем

заміщення (перевизначення) методу: зміна реалізації методу у класах-


наслідниках (сигнатура методу та сама).
С++ Java

Person p1; // def ctor Person p1 = new Person ();


Person p2 (“Ben”); Person p2 = p1; // p2 and p1 reference same obj
p1 = p2; // assignment operator p2 = null; // null reference unlike C++
Person p3 = p1; // copy c-tor p2.saySomething(); // meet
Person &p4 = p1; // reference java.lang.NullPointerException() :)
p4.saySomething();
Person *p5;
p5 = &p2 // pointer
p5->saySomething();
p5 = 0; // nullptr is better
p5 = new Person();
(*p5).saySomething();
delete p5;
p5->saySomething(); // crash?
#include <string>
class Person #include <iostream>
{ int main ()
private: {
std::string name; Person p("Bob"), p2;
Person* bestFriend; std::cout << p.greeting() << std::endl;
public: p.makeFriend(&p2);
Person(std::string n = "John Doe") std::cout << p.greeting() << std::endl;
: name(n), bestFriend(nullptr) {} }
void makeFriend(Person* f)
{ Hi, I'm Bob
bestFriend = f; Hi, I'm Bob, and my friend says: Hi, I'm
} John Doe
std::string greeting()
{
std::string greeting = "Hi, I'm " + name;
if (bestFriend) greeting += ", and my friend says: " + bestFriend->greeting();
return greeting;
}
};
class Person {
public class Main {
private String name;
public static void main(String[] args) {
private Person bestFriend;
Person p = new Person ("Bob");
public Person(String n) {
System.out.println(p.greeting());
name = n;
p.makeFriend(new Person("John Doe"));
}
System.out.println(p.greeting());
public void makeFriend(Person p) {
}
bestFriend = p;
}
}
Hi, I'm Bob
public String greeting() {
Hi, I'm Bob, and my friend says: Hi, I'm
String greeting = John Doe
"Hi, I'm " + name;
if (bestFriend != null) greeting +=
", and my friend says: " + bestFriend.greeting();
return greeting;
}
}
- механізм утворення нових класів на основі використання вже існуючих.
При цьому властивості та функціональність батьківського (супер) класу переходять до класу
нащадка (дочірнього класу)

Нагадаємо: “клас Б наслідує клас А” == “об’єкти класу Б є об’єктами класу А”

Base

base
members ...

Derived

base
members ...
(new
members …)
class Person {
private String name;
public Person(String n) { public class Main {
name = n; public static void main(String[] args) {
} Person p = new Person ("Bob");
public String greeting() { System.out.println(p.greeting());
return "Hi, I'm " + name; Student s = new Student ("Bill", 2);
} System.out.println(s.greeting());
} }
}
class Student extends Person { Hi, I'm Bob
Hi, I'm Bill, 2 grade
public int grade;
public Student(String n, int g) {
super(n);
grade = g;
}
public String greeting() {
return super.greeting() + ", " + grade + " grade";
}
}
#include <string>
class Person
{
private:
#include ”person.h” std::string name;
public:
#include <sstream> Person(std::string n) : name(n) {}
std::string greeting()
class Student : public Person
{
{
return "Hi, I'm " + name;
private:
}
int grade;
};
public:
Student(std::string n, int g) : Person(n), grade(g) {}
std::string greeting()
{
std::stringstream greeting;
greeting << Person::greeting() << ", " << grade << " grade";
return greeting.str();
}
};
#include “student.h” Видимість членів класу
public protected private
#include <iostream>
члени самого класу або друзі доступ є доступ є доступ є
int main ()
{ члени похідного класу доступ є доступ є закрито
Person p("Bob");
std::cout << p.greeting() << std::endl; інше доступ є закрито закрито
Student *s = new Student("Bill", 2);
std::cout << s->greeting() << std::endl;
Зміна видимості членів базового класу
delete s;
Наслідування / Члени public protected private
}
public = public = protected = private

Hi, I'm Bob


Hi, I'm Bill, 2 grade protected = protected = protected = private

private = private = private = private


- можливість взаємодіяти з різними типами в один спосіб через єдиний спільний інтерфейс
- здатність одного і того самого коду обробляти дані різних типів
- конкретні типи, з якими працює код, визначають поведінку програми під час її виконання

альтернатива switch-case
class Rectangle implements Shape { interface Shape {
private double height, width; public double area ();
public Rectangle(double w, double h) { }
height = h;
width = w;
}
public double area() {
return height * width;
}
}

class Ellipse implements Shape {


private double horizontalSemiAxis, verticalSemiAxis;
public Ellipse(double w, double h) {
horizontalSemiAxis = w / 2;
verticalSemiAxis = h / 2;
}
public double area() {
return 3.14 * horizontalSemiAxis * verticalSemiAxis;
}
}
class Viewer {
public static void displayArea (Shape shape) {
System.out.println(shape.area());
}
}

public class Main { інший типовий сценарій:


клієнтський код працює з множиною об'єктів різних типів,
що поелементно обробляються однаковим способом
public static void main(String[] args) { як об'єкти базового класу (або інтерфейсу).
Shape s = new Rectangle(1, 2);
Приклад: множина створених у графічному редакторі
Viewer.displayArea(s); фігур, кожну з яких слід намалювати на екрані
s = new Ellipse(2, 4); (у інтерфейсу Shape має бути метод Draw)
Viewer.displayArea(s);
}
}

2
6.28
class Shape
class Rectangle: public Shape {
{ public:
double height, width; virtual double area () const = 0;
public: };
Rectangle(double w, double h) : height(h), width(w) {}
virtual double area() const
{
return height * width;
}
};

class Ellipse: public Shape


{
double horizontalSemiAxis, verticalSemiAxis;
public:
Ellipse(double w, double h) : horizontalSemiAxis(w/2), verticalSemiAxis(h/2) {}
virtual double area() const
{
return 3.14 * horizontalSemiAxis * verticalSemiAxis;
}
};
...
#include <iostream>
class Viewer
{ клас зі статичним методом в C++ можна було б реалізувати і як звичайну процедуру
public:
static void displayArea(const Shape& shape)
{
std::cout << shape.area() << std::endl;
}
};

int main()
{
Shape *p = new Rectangle(1, 2);
Viewer::displayArea(*p);
delete p;
p = new Ellipse(2, 4);
Viewer::displayArea(*p);
delete p;
}
2
6.28
Віртуальні методи слід явно
позначити:

C++, С# : специфікатор virtual, у


базовому класі

Java: усі методи є віртуальні


base1 base2
C#, Java не підтримують множинне
наслідування від кількох класів

derived
IBase IBase2 base2

implements extends

public interface Printable {


public void print ();
derived
}

public class Document {


/* some data */
}

public class Report extends Document implements Printable {


public void print () {

/* do printing */
}
}
abstract class Document { class Document
abstract void saveAs (string fileName); {
} public:
virtual void saveAs (std::string fileName) = 0;
Document d = new Document (); // not possible // хоча б один чистий віртуальний метод
};

class RealDocument extends Document { Document d; // not possible


void saveAs (string fileName) {
class RealDocument: public Document
/* do saving */ {
} public:
} void saveAs (std::string fileName)
{
RealDocument d = new RealDocument (); /* do printing */
d.saveAs(“file1.txt”); }
};

RealDocument d;
d.saveAs(“file2.txt”);
base

base1 base2

derived

accelerate ()
{
Boat::speed++;
GasolinePoweredTransportation::speed++;

}
class Transportation
{
protected:
int speed;
};

class GasolinePoweredTransportation: virtual public Transportation


{
};

class Boat: virtual public Transportation


{
};

class MotorBoat: public GasolinePoweredTransportation, public Boat


{
public:
void accelerate()
{
speed++;
}
};
конструктори при наслідуванні, друзі, перевантаження
операторів, переліки
вже згадувалось

згадується в лекції
принципи ООП

код організація простір імен нове поняття

пакет
компіляція інтерпретація
абстракція інкапсуляція наслідування поліморфізм

віртуальна машина доступ зі зміною доступу

збирач сміття
множинне ромбовидне
на рівні пакету відкритий захищений

“дружній” віртуальне
пам'ять ресурс закритий

агрегація
типізація базові поняття відношення
стекова (авто) динамічна
композиція
жорстка

іменована змінна клас об'єкт прототип перелік


слабка

посилання вказівник
інтерфейс
члени

реалізація

статичні
ініціалізація поле метод
нестатичні

за замовчанням конструктор константні

деструктор неконстантні
копіювання

селектор

модифікатор статичне зв'язування


перевизначення

віртуальні (динамічне зв.)


присвоювання (копіювання) оператор перевантаження
Конструктори при наслідуванні. Namespace
увага: поганий стиль, засмічення простору імен
#include <string>
using namespace std;
class Person #include <iostream>
{ int main ()
private: {
string name; Student x("Ben");
public: cout << x.getName() << endl;
Person(string n = "John Doe") Person & p = x;
: name(n) {} // cout << p.getName() << endl;
protected: }
string getName() { return name; }
};
ініціалізація закритого поля
class Student : public Person через конструктор базового класу
{
public:
Student(std::string n) : Person(n) {}
string getName() { return Person::getName(); }
};
увага: заміщення невіртуального методу,
у базовому класі доступу немає
Конструктори при наслідуванні. Namespace
#include <string> краще вибірково виключати префікси,
або залишити в коді явно std::
using std::string; #include <iostream>
class Person using std::cout;
{ using std::endl;
private: int main ()
string name; {
public: Student x("Ben");
Person(string n = "John Doe") cout << x.getName() << endl;
: name(n) {} Person & p = x;
string getName() { return name; } cout << p.getName() << endl;
}; }
class Student : public Person
{
public:
using Person::Person; // альтернатива “повтору” базового конструктора
string getName() { return "Stud: " + Person::getName(); }
};

Stud: Ben
Ben
Namespace. Init list
#include <string>
namespace Example // namespaces can be nested приклад оголошення свого простору імен
{
class Sir
{ увага: порядок ініціалізації
private: визначається порядком оголошення полів у класі,
а не списком ініціалізації
std::string fullName, shortName;
public:
Sir(std::string n = "John Doe")
: shortName(n), fullName("Sir" + shortName) {}
std::string getName() { return fullName; }
};
}

#include <iostream>
int main ()
{
Example::Sir x("Ben");
помилка, значення shortName не ініціалізоване
std::cout << x.getName() << std::endl; std::bad_alloc
}
Init list. Посилання, що повертаються з методу
#include <iostream>
int main ()
{
#include <string> Example::Sir x("Ben");
namespace Example x.getName() = “Sir Ben2”;
{ std::cout << x.getName() << std::endl;
class Sir }
Sir Ben2
{
private:
std::string shortName, fullName;
public:
Sir(std::string n = "John Doe")
: shortName(n), fullName("Sir " + shortName) {}

std::string& getName() про повернення посилання замість значення


1. може дійсно зекономити ресурси
{ (не буде створюватись копія об'єктів при
//std::string tmp(fullName); поверненні з методу.
//return tmp; Хоча компілятор C++ це і так оптимізує -
return value optimization (RVO))
return fullName;
2. але неможна повертати посилання на
} тимчасові об'єкти!
}; 3. посилання на поле може використовуватись
} для його зміни
Конструктори при наслідуванні. Package
public class Main {
пакет java.lang включається за замовчанням, public static void main(String[] args) {
тобто насправді маємо Student x = new Student ("Ben");
// import java.lang.*; java.lang.System.out System.out.println(x.getName());
}
class Person { }
protected String name; Ben!
public Person(String n) {
name = n;
}
public String getName() {
return name;
} /* example of packages
} File name : Person.java */
class Student extends Person { package persons;
public Student(String n) {
super(n); interface Person {
name += "!"; public void eat();
} public void travel();
} }
Класи-друзі та функції-друзі
#include <string> увага: порушення інкапсуляції, використовуйте лише за нагальної потреби
(для забезпечення вищої продуктивності критичного коду, тощо)
class Person
{
friend class Friend;
friend void set_name(Person* p, std::string n);
private: #include <iostream>
std::string name; int main()
public: {
Person(std::string n = "John Doe") : name(n) Person p;
{} std::cout << Friend::getName(p) << std::endl;
}; set_name(&p, "Hack");
class Friend std::cout << Friend::getName(p) << std::endl;
{ }
public: John Doe
static std::string getName(Person& p) Hack
{ return p.name; }
};

void set_name(Person* p, std::string n)


{
p->name = n;
}
Перевантаження операторів
Фактично це “синтаксичний цукор” - спосіб уникнути оголошення нових методів.
struct Point Використовуйте лише там, де семантика операцій очевидна (напр. додавання векторів,
конкатенація рядків, а не віднімання “машин” чи множення “людей”)
{
float x, y;
Point::Point(float x, float y)
Point(float x = 0, float y = 0);
:x(x), y(y)
Point & operator++ ();
{}
Point operator++ (int);
Point & operator+= (const Point & p);
float & operator[](unsigned idx);
const float & operator[](unsigned idx) const;
};

std::ostream& operator<<(std::ostream& os, const Point & p);


bool operator== (const Point & a, const Point & b);
bool operator!= (const Point & a, const Point & b);
bool operator< (const Point & a, const Point & b);
bool operator>= (const Point & a, const Point & b);
Point operator+ (Point a, const Point & b);

. .* :: ?:
Не перевантажуються

sizeof typeid
Перевантаження операторів
std::ostream& operator<<(std::ostream& os, const Point & p)
{ Перевантажений оператор передачі в потік
os << "(" << p.x << "," << p.y << ")"; (напр. для спрощення виведення на екран стану
return os; об'єктів)
}

bool operator== (const Point & a, const Point & b)


{
return (a.x == b.x) && (a.y == b.y);
} Хороший тон:
Перевантажуйте одразу всю групу операцій
(логічні, арифметичні, включаючи ++, += тощо)
bool operator!= (const Point & a, const Point & b)
{ Це також дозволить визначити одні операції
return !(a == b); через інші (повторне використання коду)
}

bool operator< (const Point & a, const Point & b)


{
return (a.x * a.x + a.y * a.y) < (b.x * b.x + b.y * b.y);
}

bool operator>= (const Point & a, const Point & b)


{
return !(a < b);
}
Перевантаження операторів Point Point::operator++ (int)
{
Point & Point::operator++ () Point tmp(*this);
{ operator++();
x++; return tmp;
y++; }
return *this;
} Point operator+
Point & Point::operator += (Point a, const Point & b)
(const Point & p) {
{ a += b;
x += p.x; return a;
y += p.y; }
return *this;
} Приклад визначення одних операторів через інші
Постфіксна форма інкременту має формальний
const float & Point::operator[](unsigned idx) const параметр int для відмінності від префіксної
{
return idx ? y : x; Приклад реалізації семантики постфіксної форми
(змінити значення після використання значення)
}
float & Point::operator[](unsigned idx)
{
return const_cast<float &>(static_cast<const Point &>(*this)[idx]);
}
Перевантаження операторів

#include “Point.h”

int main(int argc, char *argv[])


{
Point a, b(1,1);
a = b;
std::cout << a << std::endl;
std::cout << (a == b) << std::endl;
std::cout << a++ << std::endl;
std::cout << ++a << std::endl;
a += b;
std::cout << (a >= b) << std::endl;
a[0] = 10;
std::cout << a[0] << ", " << a[1] << std::endl;
}
Переліки (перелічення)
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, Уникайте “магічних чисел” в коді (0, 1, 2, 3.14,
THURSDAY, FRIDAY, SATURDAY 777, -99), натомість використовуйте
} іменовані константи та їх переліки (enum)

public class EnumTest {


Day day; public static void main(String[] args) {
EnumTest firstDay = new EnumTest(Day.MONDAY);
public EnumTest(Day day) { firstDay.tellItLikeItIs();
this.day = day;
} EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
thirdDay.tellItLikeItIs();
public void tellItLikeItIs() {
switch (day) { EnumTest fifthDay = new EnumTest(Day.FRIDAY);
case MONDAY: fifthDay.tellItLikeItIs();
System.out.println("Mondays are bad.");
break; EnumTest sixthDay = new EnumTest(Day.SATURDAY);
sixthDay.tellItLikeItIs();
case FRIDAY:
System.out.println("Fridays are better."); EnumTest seventhDay = new EnumTest(Day.SUNDAY);
break; seventhDay.tellItLikeItIs();
}
case SATURDAY: case SUNDAY: }
System.out.println("Weekends are best.");
break;

default:
System.out.println("Midweek days are so-so.");
break;
Переліки (перелічення)
public enum Planet {
MERCURY (3.303e+23, 2.4397e6), public static void main(String[] args) {
VENUS (4.869e+24, 6.0518e6), double earthWeight = Double.parseDouble(args[0]);
EARTH (5.976e+24, 6.37814e6), double mass = earthWeight/EARTH.surfaceGravity();
MARS (6.421e+23, 3.3972e6), for (Planet p : Planet.values())
JUPITER (1.9e+27, 7.1492e7), System.out.printf("Your weight on %s is %f%n",
SATURN (5.688e+26, 6.0268e7), p, p.surfaceWeight(mass));
URANUS (8.686e+25, 2.5559e7), }
NEPTUNE (1.024e+26, 2.4746e7); }
Приклад розширених можливостей enum у Java
private final double mass; // in kilograms (множина об'єктів-констант)
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius; $ java Planet 175
} Your weight on MERCURY is 66.107583
private double mass() { return mass; } Your weight on VENUS is 158.374842
private double radius() { return radius; } Your weight on EARTH is 175.000000
// universal gravitational constant (m3 kg-1 s-2) Your weight on MARS is 66.279007
public static final double G = 6.67300E-11; Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
double surfaceGravity() { Your weight on URANUS is 158.397260
return G * mass / (radius * radius); Your weight on NEPTUNE is 199.207413
}
double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
Переліки (перелічення)

enum Color { red, green, blue }; // plain enum


enum Card { red_card, green_card, yellow_card }; // another plain enum
enum class Animal { dog, deer, cat, bird, human }; // enum class C++11
enum class Mammal { kangaroo, deer, human }; // another enum class

void fun() {

// examples of bad use of plain enums: // examples of good use of enum classes(safe)
Color color = Color::red; Animal a = Animal::deer;
Card card = Card::green_card; Mammal m = Mammal::deer;

int num = color; // no problem int num2 = a; // error


if (m == a) // error (good)
if (color == Card::red_card) // no problem (bad) cout << "bad" << endl;
cout << "bad" << endl;
if (a == Mammal::deer) // error (good)
if (card == Color::green) // no problem (bad) cout << "bad" << endl;
cout << "bad" << endl;
}
Надавайте перевагу enum class в C++,
це забезпечить корректну перевірку типів
Лекція 5

Частина 1. Реалізація принципів ООП у Python, Javascript, Typescript.


_________________
Частина 2. Вступ до виконання практичного завдання.
Коротка ілюстрація розробки логіки програми з подальшим переходом
до графічного інтерфейсу.
Патерн MVC. Ілюстрація ідеї розділення коду бізнес-логіки та взіємодії з
користувачем
class Rating:
def __init__(self, newid):
self.__id = newid data = Rating(1)
self.__sum = self.__mark = self.__votes = 0 # data.__mark gives error warning
# data._Rating__mark allows access anyway..
def GetVotes(self):
return self.__votes print("mark is ", data.GetMark(), " of ", data.GetVotes(), " votes")
data.AddVote(4.5)
def GetMark(self): data.AddVote(4)
return self.__mark data.AddVote(5)
print("mark is ", data.GetMark(), " of ", data.GetVotes(), " votes")
def AddVote(self, newmark): data.RemoveVote(4)
self.__sum += newmark print("mark is ", data.GetMark(), " of ", data.GetVotes(), " votes")
self.__votes += 1 data.RemoveVote(5)
self.__mark = self.__sum / self.__votes data.RemoveVote(4.5)
print("mark is ", data.GetMark(), " of ", data.GetVotes(), " votes")
def RemoveVote(self, oldmark):
if self.__votes > 1:
self.__votes -= 1
self.__sum -= oldmark mark is 0 of 0 votes
self.__mark = self.__sum / self.__votes mark is 4.5 of 3 votes
else: mark is 4.75 of 2 votes
self.__sum = self.__mark = self.__votes = 0 mark is 0 of 0 votes
from abc import ABC, abstractmethod Імпорт модуля чи пакету, або його частини

class Cypher(ABC): class CaseCypher(Cypher):


@abstractmethod def encrypt(self, text):
def encrypt(self, text): return text.swapcase()
pass
def decrypt(self, text):
@abstractmethod return self.encrypt(text)
def decrypt(self, text):
pass def test(algorithm, text): той самий “клієнтский” код,
result = algorithm.encrypt(text) що дає різні результати
class RotateCypher(Cypher): print(result) для об'єктів різних підкласів
def __init__(self, key): print(algorithm.decrypt(result)) класу Cypher
self._key = key
test(RotateCypher(3), "Hello world")
def encrypt(self, text): test(CaseCypher(), "Goodbye cruel world")
return text[self._key:] \
+ text[:self._key]

def decrypt(self, text):


lo worldHel
return text[len(text) - self._key:] \
Hello world
+ text[:len(text) - self._key]
gOODBYE CRUEL WORLD
Goodbye cruel world
модуль - програма, файл з кодом, наприклад something.py
пакет - каталог з модулями та під-пакетами приклади структури каталогів з пакетами:

імпорт модулю: import something --> виконання його коду

звернення до імен (класів, функцій тощо) в модулі після його імпорту -


через ім'я модулю: something.func1()

можна імпортувати окремі імена (складові) через конструкцію “import-from”:


from something import func1
тоді функцію можна викликати без імені модуля: func1()

імпорт пакету --> виконання коду його модулю __init__.py

якщо модуль, що імпортується може викликатися і як програма з командного рядка,


то код для виконання в режимі “окремої програми” слід розмістити в конструкції:

if __name__ == '__main__':
do_something_for_cmd_line()

якщо ж треба подібну функціональність реалізувати з пакетом somepack, то він має


містити файл __main__.py, тоді виклик його як виконуваної програми буде
таким: python -m somepack і призведе до виконання модулю somepack/__main__.py
*https://realpython.com/python-application-layouts/
**https://habr.com/ru/post/456214/
class ElectricTrafficLight(TrafficLight, ElectricDevice):
def __init__(self):
super().__init__(Color.NO_COLOR)
ElectricDevice.__init__(self)
from enum import Enum def switch(self):
if self.powered:
class Color(Enum): super().switch()
NO_COLOR = 0 else:
RED = 1 self.color = Color.NO_COLOR
YELLOW = 2 def switchOnOff(self):
GREEN = 3 super().switchOnOff()
self.switch()
class TrafficLight:
def __init__(self, col): tl = ElectricTrafficLight() При множинному
self.color = col print(tl.color) наслідуванні будується
def switch(self): tl.switchOnOff() порядок використання методів
if self.color == Color.GREEN: print(tl.color) (method resolution order, MRO)
self.color = Color.YELLOW tl.switch()
elif self.color == Color.YELLOW: tl.switch()
self.color = Color.RED print(tl.color)
else: tl.switchOnOff()
self.color = Color.GREEN print(tl.color)
print(ElectricTrafficLight.mro())
class ElectricDevice: Color.NO_COLOR
def __init__(self): Color.GREEN
self.powered = False Color.RED
def switchOnOff(self): Color.NO_COLOR
self.powered = not self.powered [<class '__main__.ElectricTrafficLight'>, <class '__main__.TrafficLight'>, <class
'__main__.ElectricDevice'>, <class 'object'>]
У JS немає окремої підтримки абстрактних класів,
перелічень, а наслідування здійснюється від 1
прототипа. Втім можливо визначити функцію, що
збиратиме прототип з кількох існуючих.

class Cypher {
class ReverseCypher extends Cypher {
encrypt(text) {
encrypt(text) {
return text;
return text.split("").reverse().join("");
}
}
decrypt(text) {
decrypt(text) {
return text;
return this.encrypt(text);
}
}
}
}
class RotateCypher extends Cypher {
function test(algorithm, text) {
constructor(key) {
result = algorithm.encrypt(text);
super();
console.log(result);
this.key = key;
console.log(algorithm.decrypt(result));
}
}
encrypt(text) {
return text.substr(this.key)
test(new RotateCypher(3), "Hello world");
+ text.substr(0,this.key);
test(new ReverseCypher(), "Goodbye cruel world");
}
decrypt(text) {
return text.substr(text.length - this.key)
lo worldHel
+ text.substr(0,text.length - this.key);
Hello world
}
dlrow leurc eybdooG
}
Goodbye cruel world
switch(): ElectricDevice {
enum Color { if (this.powered) {
NO_COLOR = "-", switch(this.color) {
RED = "R", case Color.GREEN:
YELLOW = "Y", this.color = Color.YELLOW;
GREEN = "G" break;
} case Color.YELLOW:
this.color = Color.RED;
abstract class ElectricDevice { break;
protected powered: boolean; default:
constructor() { this.color = Color.GREEN;
this.powered = false; }
} }
switchOnOff(): void { else this.color = Color.NO_COLOR;
this.powered = !this.powered; return this;
} }
abstract switch(): ElectricDevice; }
}
let tl: TrafficLight;
class TrafficLight extends ElectricDevice { tl = new TrafficLight();
color: Color; console.log(tl.color);
constructor() { tl.switchOnOff();
super(); console.log(tl.color);
this.color = Color.NO_COLOR; [LOG]: -
tl.switch().switch();
} [LOG]: G
console.log(tl.color);
switchOnOff(): void { [LOG]: R
tl.switchOnOff();
super.switchOnOff(); [LOG]: -
console.log(tl.color);
this.switch();
}
“Кухар”. Визначити ієрархію
продуктів, приготувати з них
страву - салат, розрахувати його
калорійність. Провести
сортування складових салату за
різними параметрами.
Організувати пошук продуктів у
страві.
“Хрестики-нулики”.
Реалізувати
консольну гру Х-О з В даному випадку, ідея декомпозиції коду базується на використанні патерну
іншим гравцем- MVC (model-view-controller) або MVP (model-view-presenter):
людиною, чи з
комп'ютером.
(код прикладів додається)
Масиви, динамічні масиви
#include <iostream> public class Main {
#include <string> public static void main(String[] args) {
int main() int m[] = new int [3];
{ int []n = { 1, 2 };
int m[5] = { 1, 2, 3, 4, 5 }; for (int i = 0; i < 2; ++i)
std::string * array; System.out.println(n[i]);
std::string ** matrix;
for (int i = 0; i < 5; ++i) String [] array;
std::cout << m[i] << std::endl; array = new String [5];
String [] array2 = {"1", "2", "3"};
array = new std::string[5]; for (String i : array2)
for (auto i = 0; i < 5; ++i) System.out.println(i);
std::cout << array[i] << std::endl;
delete [] array; int matrix[][] = new int [5][5];
for (int i = 0; i < matrix.length; ++i)
matrix = new std::string* [2]; matrix[i][i] = i + 1;
for (int i = 0; i < 2; ++i)
matrix[i] = new std::string[i + 1]; int matrix2[][] = { {1,2}, {3,4}};
int [][] matrix3 = new int [2][];
for (int i = 0; i < 2; ++i) for (int i = 0; i < matrix3.length; ++i)
delete [] matrix[i]; matrix3[i] = new int [i + 1];
delete [] matrix;
} }
}
Варіації new throwing (1):
void* operator new (std::size_t size) throw (std::bad_alloc);

#include <iostream> nothrow (2):


void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
#include <new>
placement (3):
void* operator new (std::size_t size, void* ptr) throw();
struct MyClass {
int data[100];
MyClass() {std::cout << "constructed [" << this << "]\n";}
};

int main () {
MyClass * p1 = new MyClass;
MyClass * p2 = new (std::nothrow) MyClass;
new (p2) MyClass; // новий об’єкт за адресою старого
p2->~T(); // явно викликаємо деструктор
MyClass * p3 = (MyClass*) ::operator new (sizeof(MyClass)); // тут конструктор
не викликається. “Звичайний” new викликає operator new + конструктор

}
Перевантаження new
#include <iostream>
#include <iostream>
struct X {
#include <algorithm>
static void* operator new(std::size_t sz)
{
void* operator new[](std::size_t sz, char c)
std::cout << "custom new for size " << sz << '\n';
{
return ::operator new(sz);
void* p = operator new[](sz);
}
std::fill_n(reinterpret_cast<char*>(p), sz, c);
static void* operator new[](std::size_t sz)
return p;
{ дещо з того, що ще буде розглянуто:
} приведення типів та алгоритми
std::cout << "custom new[] for size " << sz << '\n';
return ::operator new(sz);
int main()
}
{
};
char* p = new('*') char[6];
int main() {
p[5] = '\0';
X* p1 = new X;
std::cout << p << '\n';
delete p1;
delete[] p;
X* p2 = new X[10];
}
delete[] p2;
}
Можливе “ручне” розміщення масиву у пам'яті
#include <new>
class A
{
public:
A(int x){}
~A(){}
};
const int n = 50;
A* placementMemory = static_cast<A*>(operator new[] (n * sizeof(A)));
for (int i = 0; i < n; i++)
{
new (placementMemory + i) A(rand());
}
for (int i = 0; i < n; i++)
{
placementMemory[i].~A();
}
operator delete[] (placementMemory);
Memory leaks - втрачена пам'ять

int* p = new int(7);


p = nullptr; // memory leak

void f()
{
int* p = new int(7);
} // memory leak

void f()
{ Виключні ситуації будуть розглянуті в наступній лекції. Ідея в тому,
int* p = new int(7); що при виключних ситуаціях виконання блоку коду переривається
g(); // тут може бути throw та оператор delete може залишитись невиконаним
delete p;
} // memory leak якщо виключення на g()
Віртуальний деструктор На замітку: Правило ТРЬОХ
якщо у класі визначається хоча б один з цих методів:
- деструктор
- конструктор копіювання
- оператор присвоювання
class Derived то слід визначити усі три.
class Base { Важливо, коли клас динамчіно взаємодіє з ресурсами
{ public:
public: Derived(int size)
Base() {
{ data = new int [size];
for (auto i = 0; i < 10; ++i) }
data[i] = i; ~Derived()
} {
private: delete [] data;
int data [10]; }
} private:
увага: якщо деструктор не віртуальний, при роботі
int [] data;
через вказівник на базовий клас буде викликаний }
лише деструктор базового класу,
і пам'ять не буде вивільнено (в найкращому випаку)
int main()
Оголошуйте деструктор (навіть порожній) як віртуальний, {
якщо клас передбачає наслідування Base *p = new Derived(10);
delete p;
}
Віртуальний деструктор
class Derived: public Base
{
#include <iostream>
public:
Derived(int size)
{
class Base
data = new int[size];
{
std::cout << "Derived()\n";
public:
}
Base()
~Derived()
{
{
for (auto i = 0; i < 10; ++i)
delete[] data;
data[i] = i;
std::cout << "~Derived()\n";
std::cout << "Base()\n";
}
}
private:
virtual ~Base()
int * data;
{
};
std::cout << "~Base()\n";
}
int main()
private:
{ результат:
int data[10]; Base()
Base *p = new Derived(10);
}; Derived()
delete p; ~Derived()
} ~Base()
Код без RAII (Resource acquisition is initialization)

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/

File f;
f.open("boo.txt");
//UNSAFE - an exception here means the file is never closed
loadFromFile(f);
f.close();

Dog* dog = new Daschund;


//UNSAFE - an exception here means the dog is never deleted
goToThePark(dog);
delete dog;

Lock* lock = getLock();


lock.aquire();
//UNSAFE - an exception here means the lock is never released
doSomething();
lock.release();
Парадигма RAII (Resource acquisition is initialization)
Важливо, коли клас динамчіно взаємодіє з ресурсами

Захоплення об'єкту (напр. виділення пам'яті чи відкриття файлу) - у конструкторі,


class OpenFile { вивільнення - у деструкторі.
public:
При виникненні виключення усі автоматичні змінні будуть знищені.
OpenFile(const char* filename){ Для автоматичних об'єктів це означатиме виклик деструктора та, відповідно,
//throws an exception on failure вивільнення ресурсів.
_file.open(filename);
}
// Використання:
~OpenFile(){
OpenFile f("boo.txt");
_file.close();
loadFromFile(f);
}

std::string readLine() {
return _file.readLine();
}

private:
File _file;
};
“Розумні вказівники” (smart pointers). auto_ptr
Найпростіший розумний вказівник,
обгортка для звичайного вказівника в автоматичній пам'яті

При присвоюванні між авто-вказівниками право володіння


#include <iostream> передається від старого власника до нового.
#include <memory>

int main () {
std::auto_ptr<int> p1 (new int);
// або так:
*p1.get()=10;
std::auto_ptr<int> x_ptr(new int(42));
std::auto_ptr<int> p2 (p1);
std::auto_ptr<int> y_ptr;
std::cout << "p2 points to " << *p2 << '\n';
y_ptr = x_ptr; // !!!
// (p1 is now null-pointer auto_ptr)
// segmentation fault
return 0;
std::cout << *x_ptr << std::endl;
}
Розумні вказівники є шаблонами (templates).
Шаблони є реалізацією парадигми узагальненого програмування,
коли алгоритми описуються незалежними від типів даних, що вони
обробляють. Шаблони та їх бібліотеки будуть розглянуті пізніше.
Синтактично конкретний тип даних, для яких використовується шаблон,
вказується у <>
“Розумні вказівники” (smart pointers). unique_ptr
Додатковий захист від випадкового присвоювання
std::unique_ptr<int> x_ptr(new int(42)); Див. також фнкцію make_unique

std::unique_ptr<int> y_ptr;

// y_ptr = x_ptr; помилка компіляції


y_ptr = std::move(x_ptr); // явна передача володіння

///////////////// що таке std::move? далі буде...

template <class T> swap(T& a, T& b) // старий варіант


{
T tmp(a); // now we have two copies of a
a = b; // now we have two copies of b
b = tmp; // now we have two copies of tmp (aka a)
}
template <class T> swap(T& a, T& b) // тепер без копій, якщо можливо:
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
shared_ptr
Реалізація спільного володіння. Пам'ять вивільняється при знищенні
останнього розумного вказівника, для чого ведеться підрахунок посилань (користувачів).
Молива проблема - циклічні залежності

#include<iostream> int main()


#include<memory> {
using namespace std; shared_ptr<A> p1 (new A);
cout << p1.get() << endl; // 0x1c41c20
class A p1->show(); // A::show()
{ shared_ptr<A> p2 (p1);
public: p2->show(); // A::show()
void show() cout << p1.get() << endl; // 0x1c41c20
{ cout << p2.get() << endl; // 0x1c41c20
cout<<"A::show()"<<endl; cout << p1.use_count() << endl; // 2
} cout << p2.use_count() << endl; // 2
}; p1.reset();
cout << p1.get() << endl; // 0
cout << p2.use_count() << endl; // 1
cout << p2.get() << endl; //0x1c41c20
}
shared_ptr без weak_ptr
https://www.learncpp.com/cpp-tutorial/15-7-circular-dependency-issues-with-stdshared_ptr-and-stdweak_ptr/

#include <iostream>
#include <memory> // for std::shared_ptr
#include <string>

class Person Результат - об’єкти не знищені:


{
std::string m_name; Lucy created
std::shared_ptr<Person> m_partner; Ricky created
public: Lucy is now partnered with Ricky
Person(const std::string &name): m_name(name)
{ std::cout << m_name << " created\n"; }
~Person() {std::cout << m_name << " destroyed\n";}

friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)


{
if (!p1 || !p2) return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
};
int main()
{
auto lucy = std::make_shared<Person>("Lucy"); // create a Person named "Lucy"
auto ricky = std::make_shared<Person>("Ricky"); // create a Person named "Ricky"
partnerUp(lucy, ricky); // Make "Lucy" point to "Ricky" and vice-versa
}
shared_ptr + weak_ptr
#include <iostream> https://www.learncpp.com/cpp-tutorial/15-7-circular-dependency-issues-with-stdshared_ptr-and-stdweak_ptr/
#include <memory>
#include <string> - weak_ptr не вважається власником,
лише спостерігачем
class Person
{
- метод lock() створює та повертає Результат:
std::string m_name;
shared_ptr Lucy created
std::weak_ptr<Person> m_partner;
public: Ricky created
Person(const std::string &name): m_name(name) Lucy is now partnered with Ricky
{ std::cout << m_name << " created\n"; } Ricky's partner is: Lucy
~Person() {std::cout << m_name << " destroyed\n";} Ricky destroyed
Lucy destroyed
friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
{
if (!p1 || !p2) return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
const std::shared_ptr<Person> getPartner() const { return m_partner.lock(); }
const std::string& getName() const { return m_name; }
}
int main()
{
auto lucy = std::make_shared<Person>("Lucy");
auto ricky = std::make_shared<Person>("Ricky");
partnerUp(lucy, ricky);
auto partner = ricky->getPartner(); // get shared_ptr to Ricky's partner
std::cout << ricky->getName() << "'s partner is: " << partner->getName() << '\n';
return 0;
}
R-value, L-value
#include <iostream>
using namespace std;
int main()
{
int x = 3 + 4; // 3 + 4 - rvalue, x - lvalue (не тимчасова змінна, має адресу)
cout << x << endl;
}

int main()
{
int i, j, *p;
// Correct usage: the variable i is an lvalue.
i = 7;
// Incorrect usage: The left operand must be an lvalue (C2106).
7 = i; // C2106
j * 4 = 7; // C2106
// Correct usage: the dereferenced pointer is an lvalue.
*p = i;
const int ci = 7;
// Incorrect usage: the variable is a non-modifiable lvalue (C3892).
ci = 9; // C3892
// Correct usage: the conditional operator returns an lvalue.
((i < 3) ? i : j) = 7;
}
R-value reference (C++11)
#include <iostream>
#include <string>

int main() {
std::string s1 = "Test";
// std::string&& r1 = s1; // error: can't bind to lvalue

const std::string& r2 = s1 + s1; // okay: lvalue reference to const extends lifetime


// у VS також можна без const, але це не стандарт
// r2 += "Test"; // error: can't modify through reference to const

std::string&& r3 = s1 + s1; // okay: rvalue reference extends lifetime


r3 += "Test"; // okay: can modify through reference to non-const
std::cout << r3 << '\n';
}
Приклад https://www.internalpointers.com/post/c-rvalue-references-and-move-semantics-beginners

class IntBox
// додамо по “правилу трьох”:
{
IntBox(const IntBox& other)
public:
{
IntBox(int size)
m_data = new int[other.m_size];
{
std::copy(other.m_data, other.m_data + other.m_size, m_data);
m_data = new int[size];
m_size = other.m_size;
m_size = size;
}
}
IntBox& operator=(const IntBox& other)
~IntBox()
{
{
if(this == &other) return *this;
delete[] m_data;
delete[] m_data;
}
m_data = new int[other.m_size];
private:
std::copy(other.m_data, other.m_data + other.m_size, m_data);
m_size = other.m_size;
int* m_data;
return *this;
size_t m_size;
}
};
Приклад, доповнення На замітку: Правило П'ЯТЬОХ
якщо у класі визначається хоча б один з цих методів:
- деструктор
- конструктор копіювання
class IntBox IntBox(IntBox&& other)
- оператор присвоювання
{ { - конструктор переміщення
public: m_data = other.m_data; - оператор переміщення
IntBox(int size) m_size = other.m_size;
то слід визначити усі п'ять.
{ other.m_data = nullptr; Важливо, коли клас динамчіно взаємодіє з ресурсами
m_data = new int[size]; other.m_size = 0; Правило 5ти доповнює правило 3х для кращої
m_size = size; } оптимізації керування ресурсами
} IntBox& operator=(IntBox&& other)
Правило ШЕСТИ: згадується конструктор
~IntBox() { за замовчанням (без параметрів)
{ if (this == &other) return *this;
delete[] m_data; delete[] m_data; На замітку: Правило НУЛЯ
} m_data = other.m_data; якщо у клас не потребує керування ресурсами, бо це
private: m_size = other.m_size; вже реалізовано іншими класами (напр. об'єкти яких
входять до даного класу як поля), то не слід
other.m_data = nullptr; визначати жодного елемента з “правила 6ти”
int* m_data; other.m_size = 0;
size_t m_size; return *this;
}; }

Примітка: RVO (Return Value Optimization) має вужче застосування: при поверненні значення з методу.
Оптимізація через семантику переміщення може застосовуватись і при передачі тимчасових об’єктів-аргументів
Ще приклад... Використання swap

class dense_matrix dense_matrix& dense_matrix::operator= (dense_matrix&& other)


{ {
public: if (this == &other) return *this;
std::size_t rows_count; swap(*this, other);
std::size_t cols_count; return *this;
double** data; }

dense_matrix(std::size_t rows_count, std::size_t cols_count = 1); dense_matrix::dense_matrix(dense_matrix&& orig)


... {
dense_matrix(dense_matrix&& orig); swap(*this, orig);
dense_matrix& operator=(dense_matrix&& other); }
private:
friend void swap(dense_matrix& first, dense_matrix& second)
{
using std::swap;
swap(first.rows_count, second.rows_count);
Приклад використання 1 Приклад використання 2
swap(first.cols_count, second.cols_count);
dense_matrix work; dense_matrix createMx()
swap(first.data, second.data);
{ {
}
dense_matix tmp; dense_matix tmp;
};
.... ....
work = tmp; return tmp; // не розраховуємо на RVO
} }
// continue with work
Мови, де оперують посиланнями...

import copy as cp Java: див. clone(), deep copy, shallow copy

class RallyTeam: У мовах, де оперують посиланнями, немає можливості розрізняти різну


def __init__(self, driver, co_driver): семантику оператора присвоювання.
self.driver = driver
self.co_driver = co_driver Як правило, копіювання за замовчанням - це копіювання кожного поля об'єкту класу,
тоюто копіювання посилань (на той самий об'єкт у пам'яті).
class Person:
def __init__(self, name=''): “Реальне” копіювання даних називають клонуванням.
self.name = name Причому може бути клонування поверхове чи глибинне (глибоке, коли клонування
рекурентно торкається усіх полів об'єкта)
t1 = RallyTeam(Person('Bob'),Person('Bill'))

t2 = cp.copy(t1)
t1.driver.name = 'Ben'
print(t2.driver.name)

t2 = cp.deepcopy(t1)
t1.co_driver.name = 'Bart'
print(t2.co_driver.name)
Виключення C++

Виключення в конструкторі дозволяють сповістити


int main () { про помилки під час створення об’єктів.
try
{ Але в той же час виключення
НЕ ПОВИННІ ЗАЛИШАТИ МЕЖІ деструктора!
throw 20;
}
catch (int e)
{
cout << "An exception occurred. Exception Nr. " << e << '\n';
}
return 0;
} Поширення виключення назовні (re-throw, propagation):
---------------------------------------
try { try
{
// code here ... // some code here
} }
catch (int param) { cout << "int exception"; } catch (const SomeException& e)
{
catch (char param) { cout << "char exception"; } std::cout << "caught an exception";
catch (...) { cout << "default exception"; } throw;
}
Виключення C++, їх наслідування
#include <iostream>
#include <exception>
using namespace std;

class myexception: public exception


{
virtual const char* what() const throw()
{
return "My exception happened";
}
} myex;

int main () {
try
{
throw myex;
}
catch (exception& e)
{
cout << e.what() << '\n';
}
return 0;
Виключення Java
try {
// Protected code
} catch (ExceptionType1 e1) {
// Catch block
} catch (ExceptionType2 e2) {
// Catch block
} catch (ExceptionType3 | ExceptionType4 e3) {
// Catch block
} finally {
// The finally block always executes.
}
-------------------------------
import java.io.*;
public class className {
тип виключення - частина оголошення методу

public void deposit(double amount) throws RemoteException {


// Method implementation
throw new RemoteException();
}
// Remainder of class definition
}
Виключення Java (try-with-resources)
import java.io.File;
import java.io.FileReader; try(FileReader fr = new FileReader("E://file.txt")) {
import java.io.IOException;
(в C++ використовують RAII)

public class ReadData_Demo {


public static void main(String args[]) {
FileReader fr = null;
try {
File file = new File("file.txt");
fr = new FileReader(file);
char [] a = new char[50];
fr.read(a); // reads the content to the array
for(char c : a)
System.out.print(c); // prints the characters one by one
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
fr.close();
} catch (IOException ex) {
ex.printStackTrace();
}
Виключення в інших мовах

try def divide(x, y): try {


{ try: alert( 'try' );
int x = 5; result = x / y } catch (e) {
int y = x / 0; except ZeroDivisionError: alert( 'catch' );
} print("division by zero!") ...
catch raise throw e; // re-throw if needed
{ else: } finally {
Console.WriteLine("ZeroDiv!"); print("result is", result) alert( 'finally' );
throw; finally: }
} print("executing finally clause")
finally
{
Console.WriteLine("finally");
}
C++ const, volatile / Java final public class Sphere {

public static final double PI =


3.141592653589793;
cv-qualifiers: const & volatile
volatile: не оптимізувати public final double radius;
const: не змінювати public final double xPos;
public final double yPos;
void Class::getSomething() const public final double zPos;
{ // тут жодних спроб змінити дані this
} Sphere(double x, double y,
double z, double r) {
int* - pointer to int radius = r;
int const * - pointer to const int xPos = x;
int * const - const pointer to int yPos = y;
int const * const - const pointer to const int zPos = z;
const int * == int const * }
const int * const == int const * const [...]
int ** - pointer to pointer to int }
int ** const - a const pointer to a pointer to an int
int * const * - a pointer to a const pointer to an int
int const ** - a pointer to a pointer to a const int
int * const * const - a const pointer to a const pointer to an i
Приведення типів
const float & Point::operator[](unsigned idx) const
буває явним та неявним (напр. в C++ неявно приводяться {
базові типи даних) return idx ? y : x;
}
int a = 5.6; // warning float & Point::operator[](unsigned idx)
float b = 7; {
return const_cast<float &>(static_cast<const Point &>(*this)[idx]);
int a = static_cast<int>(7.5); }

void someFunction( const Foo& f )


{
зняття константності
Foo &fRef = const_cast<Foo&>(f);
}
приведення типів “згори вниз” по ієрархії наслідування

class1* p1 = dynamic_cast<class1*> (&obj); // NULL if error


class2& r2 = dynamic_cast<class2&> (obj); // exception if error як правило, не найкраща практика,
бажано переглянути ієрархію класів
BaseObject** objects;
int a = 0xffe38024; ...
int* b = reinterpret_cast<int*>(a); if (DerivedObject* obj = dynamic_cast < DerivedObject* > (objects[i]))
{
синтаксис C, Java, C#: useDerived(*obj);
}
(type) expression else if (typeid(objects[i]) == typeid(BaseObject*)) // інший варіант, використання RTTI
{
useBase((BaseObject&)*objects[i]);
}
explicit (C++)
(явні конструктори, що не конвертуються або явні операції приведення типів)

struct A
{
A(int) { } // converting constructor
A(int, int) { } // converting constructor
operator bool() const { return true; }
};

struct B
{
explicit B(int) { }
explicit B(int, int) { }
explicit operator bool() const { return true; }
};
explicit (C++)
int main()
{
A a1 = 1; // OK: copy-initialization selects A::A(int)
A a2(2); // OK: direct-initialization selects A::A(int)
A a3 {4, 5}; // OK: direct-list-initialization selects A::A(int, int)
A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
A a5 = (A)1; // OK: explicit cast performs static_cast
if (a1) ; // OK: A::operator bool()
bool na1 = a1; // OK: copy-initialization selects A::operator bool()
bool na2 = static_cast<bool>(a1); // OK: static_cast performs direct-initialization

// B b1 = 1; // error: copy-initialization does not consider B::B(int)


B b2(2); // OK: direct-initialization selects B::B(int)
B b3 {4, 5}; // OK: direct-list-initialization selects B::B(int, int)
// B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
B b5 = (B)1; // OK: explicit cast performs static_cast
if (b2) ; // OK: B::operator bool()
// bool nb1 = b2; // error: copy-initialization does not consider B::operator bool()
bool nb2 = static_cast<bool>(b2); // OK: static_cast performs direct-initialization
}
mutable (C++)
(може змінюватись для константних об’єктів чи константними методами)

class Calculus
{
mutable int cachedValue;
mutable bool isChanged;
...
public:
int calculate() const
{
if(!isChanged) return cachedValue;

cachedValue = ...; // тривалі


розрахунки
isChanged = false;
}
...
};
final method / class final class NoExtending {
// …
}
struct Base
{ class Test {
virtual void foo(); final void getSomething() {
}; }
...
struct A : Base }
{
void foo() final; // A::foo is overridden and it is the final override
void bar() final; // Error: non-virtual function cannot be overridden or be final
};

struct B final : A // struct B is final


{
void foo() override; // Error: foo cannot be overridden as it's final in A
};

struct C : B // Error: B is final


{
};
override

struct A
// Java:
{
// mark method as a superclass method
virtual void foo();
// that has been overridden:
void bar();
};
@Override
int overriddenMethod() { }
struct B : A
{
void foo() const override;
// Error: B::foo does not override A::foo
// (signature mismatch)

void foo() override; // OK: B::foo overrides A::foo


void bar() override; // Error: A::bar is not virtual
};
default (C++)
class Foo
{
public:
Foo() = default; // згенерувати за замовчанням (аналог Foo(){} )
Foo(int x) {/* ... */}
};

застосовний до:
- конструктор за замовчанням (без агрументів),
- конструктор копіювання,
- конструктор переміщення
- оператор присвоювання,
- оператор переміщення
- деструктор
delete (C++)
заборона використання методу, дозволяє не ховати його у private з ризиком
все ж викликати цей метод з інших методів класу

class Foo
{
public:
Foo() = default;
Foo(const Foo&) = delete;
void bar(int) = delete;
void bar(double) {}
};

class Foo
{
public:
void *operator new(std::size_t) = delete;
void *operator new[](std::size_t) = delete;
};
Вкладені (inner, nested) класи

про доступ:
struct enclose
{
class Enclosing {
struct inner private:
{ int x;
static int x;
class Nested {
void f(int i);
int y;
}; void NestedFun(Enclosing *e) {
}; cout << e->x; // ОК, вкладений клас - звичайний член зовнішнього класу
int enclose::inner::x = 1; // definition }
};
void enclose::inner::f(int i) {} // definition public:
void EnclosingFun(Nested *n) {
cout<< n->y; // Не ОК: y для зовнішнього класу закритий
}
};
Вкладені (inner, nested) класи
class OuterClass { - локальні класи:

private int outerField; class OuterClass {

class InnerClass { private int outerField;


int getOuterField() { void methodWithLocalClass() {
return class InnerLocalClass {
OuterClass.this.outerField; void getOuterField() {
} int a = outerField;
} …
} }
}
OuterClass.InnerClass inner = InnerLocalClass innerLocal = new InnerLocalClass();
new OuterClass().new InnerClass(); }
}
- можна вкладати статичні класи

Екземпляр статичного вкладеного класу може бути створений без створення екземпляру зовнішнього класу.
Статичний вкладений клас має доступ лише до статичних членів зовнішнього класу.
Нестатичний вкладений клас (внутрішній клас) має доступ і до нестатичних членів зовнішнього класу.
Анонімний клас Java
void methodWithAnonymousClass(final int interval) {
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("action!");
}
};

Анонімний клас C++


class
{
int i;
public:
void setData(int i)
{
this->i = i;
}
void print()
{
cout << "Value for i : " << this->i << endl;
}
} obj1;
Лямбда-функції
[capture](parameters) -> return-type{body} (parameters) -> {body}

void foo() List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);


{ for(Integer n: list) {
auto f = [](int a, int b) -> int { System.out.println(n);
return a + b; }; }
auto n = f(1, 2);
} List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

до C++11 - функтори list.forEach(n -> System.out.println(n));


(клас або його об'єкт з перевантаженим
оператором ()): // скорочена форма через посилання на метод
list.forEach(System.out::println);
void bar()
{ Лямбда-фукнції - спосіб (конструкція) оголошення анонімних функцій.
struct LocalClass Інколи (JS) називаються стрілочними функціями.
{ Якщо анонімна функція посилається на змінні, що не оголошені
в її тілі (захоплення), то вона часто називається замиканням.
int operator()(int a, int b) const { return a + b; }
} f;
int n = f(1, 2);
}
Патерн проектування - архітектурна конструкція, застосовна до вирішення певних типових проблем проектування архітектури ПЗ
- не є готовим рецептом! важливий контекст конкретної задачі
- є певним зразком, що є одним з кращих рішень своєї типової задачі.

Класикою є GoF - патерни. Але перед цим:

SOLID — п'ять “основоположних” принципів ОО-проектування

S SRP Принцип єдиного обов'язку


Кожен об'єкт має виконувати лише один обов'язок.

O OCP Принцип відкритості/закритості


Програмні сутності повинні бути відкритими для розширення, але закритими для
змін.

L LSP Принцип підстановки Барбари Лісков


Об'єкти в програмі можуть бути заміненими їх нащадками без зміни коду
програми.

I ISP Принцип розділення інтерфейсу


Багато спеціалізованих інтерфейсів краще за один універсальний.

D DIP Принцип інверсії залежностей


Залежності всередині системи будуються на основі абстракцій, що не повинні
залежати від деталей; навпаки, деталі мають залежати від абстракцій. Модулі
вищих рівнів не залежать від модулів нижчих рівнів.
Анти-патерни
Інверсія абстракції (Abstraction inversion) — приховування (простої та корисної -
“велосипедної”) частини функціональності від зовнішнього використання, в надії на те, що ніхто
не буде її використовувати.

Перевантажений інтерфейс: Проектування наскільки потужного інтерфейсу, що його вкрай


важко реалізовувати.

Магічна кнопка (Magic pushbutton): Кодування логіки реалізації класу безпосередньо в коді
елементів інтерфейсу, без використання абстракції.

Стан гонитви (Race hazard): Результат програми залежить від послідовності неконтрольованих
подій.

Зайнятий очікуванням (Busy waiting): Використання CPU під час очкування якоїсь дії,
зазвичай для тривалих циклів перевірки, замість використання повідомлень про події

Приховування помилок (Error hiding): Перехоплення повідомлень, що свідчать про помилку, до


того, як вони надійдуть до користувача, замість реального виправлення помилок.

Жорсткий код (Hard code): Вкладення припущень про середовище системи у її реалізації.

М'який код (Soft code): Зберігання бізнес-логіки у конфігураційних файлах, а не у коді


Анти-патерни
Магічні числа (Magic numbers): Включення непрокоментованих та незадокументованих констант в алгоритм.

Повторення коду (Repeating yourself): Написання коду, який повторюється знову і знову, замість організації підпрограм.

Програмування вставкою (Copy and paste programming): Копіювання (і зміна) існуючого коду, замість того, щоб створювати нове
рішення.

Код Спагеті (Spaghetti code): Програми, структури яких ледь зрозумілі через неправильне застосування структур коду.

Код Лазаньї (Lasagna code): Програма, структура якої містить забагато рівнів.

Передчасна оптимізація (Premature optimization): Завчасне програмування для підвищення ефективності, жертвуючи хорошим
дизайном, гнучкістю, а інколи навіть реальною ефективністю.

BaseBean: Спадкування функціональності із службового класу, а не делегування йому вирішення проблем

Проблема “коло-еліпс” (Circle-ellipse problem): Некоректне використання механізму наслідування.

Циклічні залежності (Circular dependency): Представлення непотрібних прямих чи непрямих взаємних залежностей між об'єктами та
модулями програми.

Божественний об'єкт (God object): Концентрування функціоналу в одній частині проекту (класу, методі).

Полтергейсти (Poltergeists): Недовговічні об'єкти, єдиною метою яких є ініціалізація інших об'єктів (назва типу "manager_", "controller_",
"start_process", ...).

Послідовне з'єднання (Sequential coupling): Клас вимагає, щоб його методи викликалися у певному порядку
GoF патерни
Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. (“Gang of Four”).
Приемы объектно-ориентированного проектирования. Паттерны
проектирования
Приклади: одинак (singleton)
Задача: забезпечити існування лише одного екземпляру об'єкта

Класичний

//.h Сінглтон Майєрса (по C++11 - потоково-безпечний)


class Singleton
{ class OnlyOne
public: {
static Singleton* Instance(); public:
protected: static OnlyOne& Instance()
Singleton(); {
private: static OnlyOne theSingleInstance;
static Singleton* _instance; return theSingleInstance;
} }
private:
//.cpp OnlyOne(){}
Singleton* Singleton::_instance = 0; OnlyOne(const OnlyOne& root) = delete;
Singleton* Singleton::Instance() { OnlyOne& operator=(const OnlyOne&) = delete;
if(_instance == 0){ };
_instance = new Singleton;
}
return _instance;
}
Приклади: фасад (facade)
Задача: приховати складну архітектуру за простим відкритим інтерфейсом
Узагальнене програмування
Типи поліморфізму:
Узагальнене програмування (англ. generic programming) —
парадигма програмування, що полягає в такому описі даних і - ad-hoc: перевантаження методів,
алгоритмів, який можна застосовувати до різних типів даних, не різний код під різні конкретні аргументи, одне ім'я
змінюючи сам опис методу

- параметричний: один код під довільні аргументи


Приклад: застосування препроцесора для узагальненого (код може не компілюватися, якщо певні типи
програмування аргументів з ним несумісні). Templates/generics

- через наслідування (“за замовчанням” в цьому


#include <stdio.h> курсі): один інтерфейс, різна реалізація у
підкласах.
#define SWAP(_a, _b, type) { type _c; _c = _b; _b = _a; _a = _c; }

int main() {
int a=1, b=4; Макроси - не найкраща ідея:
double c=2.3, d=3.4; - проблеми з відлагодженням
- розширення макро-визначень іноді дає дивні результати:
SWAP(a, b, int); SWAP(a++,b++,int) --> ?
printf("%i %i\n", a, b); - немає перевірки типів (можна випадково передати хибні
SWAP(c, d, double); параметри і код не міститиме синтаксичних помилок)
printf("%f %f\n", c, d);
return 0;
}
Пригадуємо динамічні структури даних у C...
Спроба універсалізації

class Box {
private Object object;

public void set(Object object) { this.object = object; }


public Object get() { return object; }
}

...
public class Main {
public static void main(String[] args) {
Box b = new Box();
// b.set(10);
b.set(new Integer(10).toString());
String s = (String)b.get(); // Runtime-помилка, якщо b.set(10)
s += "test";
System.out.println(s);
}
}
Java Generics (параметризовані типи)
class Box<T> {
// T - параметр типу
private T t;

public void set(T t) { this.t = t; }


public T get() { return t; }
}

public class Main {


public static void main(String[] args) {
Box<String> b = new Box<String>(); // String тут є аргументом типу
// b.set(10); // Compile-time помилка
b.set(new Integer(10).toString());
String s = b.get();
s += "test";
System.out.println(s);
Box<Integer> b2 = new Box<>(); // “Diamond-форма”: виведення типу
b2.set(10); // “auto-boxing” в Integer.valueOf(10)
}
}
Java Generics (параметризовані типи)
public interface Pair<K, V> {
public K getKey();
public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

private K key;
private V value;

public OrderedPair(K key, V value) {


this.key = key;
this.value = value;
}

public K getKey() { return key; }


public V getValue() { return value; }
}
Java Generics (параметризовані типи)
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");

OrderedPair<String, Integer> p3 = new OrderedPair<>("Even", 8);


Pair<String, String> p4 = new OrderedPair<>("hello", "world");

допускається і параметризовані типи як аргументи типу:

OrderedPair<String, Box<Integer>> p =
new OrderedPair<>("primes", new Box<Integer>(3)); // якщо додати конструктор
Параметризовані методи
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

або:

boolean same = Util.compare(p1, p2);


Обмежені параметри типу
public class Box<T> {
...
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
}
...
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
// integerBox.inspect("some text"); так не можна
}
Обмежені параметри типу
public class NaturalNumber<T extends Integer> {

private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }


class D <T extends B & A & C> { /* ... */ } // compile-time error
Type Erasure (механізм реалізації)
public class Node {
public class Node<T> {
private Object data;
private T data; private Node next;
private Node<T> next;
public Node(Object data, Node next)
public Node(T data, Node<T> next) {
{ this.data = data;
this.data = data; this.next = next;
this.next = next; }
}
public Object getData() { return
public T getData() { return data; } data; }
// ... // ...
} }
Type Erasure (механізм реалізації)
public class Node {
public class Node<T extends
Comparable<T>> { private Comparable data;
private Node next;
private T data;
private Node<T> next; public Node(Comparable data, Node
next) {
public Node(T data, Node<T> next) this.data = data;
{ this.next = next;
this.data = data; }
this.next = next;
} public Comparable getData()
{ return data; }
public T getData() { return data; } // ...
// ... }
}
Type Erasure (механізм реалізації)

https://hajsoftutorial.com/java-bridge-method/
С++ Templates (шаблони)
template <typename T>
class Unsigned
{
public:

Unsigned(T number = 0)
: number(number < 0 ? -number : number)
{}
operator T() const { return number; }
Unsigned & operator *= (Unsigned multiplier)
{
number *= multiplier.number;
return *this;
}

private:

T number;
};
С++ Templates (шаблони)

template <class T>


Unsigned<T> operator* (Unsigned<T> left, Unsigned<T> right)
{
left *= right;
return left;
}

double number = -5.5;


Unsigned<double> un = number;
std::clog << un << std::endl;
Параметри шаблонів
template <class T1, // параметр-тип
typename T2, // параметр-тип
int I, // не типізуючий параметр
typename T3 = int, // тип за замовчанням при <>
template<class> class T3 // параметр-шаблон
>
При використанні шаблонних класів чи функцій з конкретними параметрами
компілятор створює їх спеціалізації
Тому шаблони слід повністю визначати у заголовочних файлах,
а не розбивати на h та cpp частини (можна використати псевдо-реалізації у
hpp файлах, але це не змінює суті). Як виключення у cpp-файлі можуть бути
спеціалізовані реалізації:

template <typename T>


void foo::do(const T& t)
{
// Do something with t
}
template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);
Приклади

template< typename T > template< typename T >


void func1( T a[], int n, T x, T y) void func2( T a[], int n )
{ {
for (int i = 0; i < n; i++) for (int i = 0; i < n / 2; i++)
{ {
if (a[i] == x) T t;
a[i] = y; t = a[n - 1 - i];
} a[n - 1 - i] = a[i];
} a[i] = t;
}
}
Стандартні шаблони

template <class ForwardIterator, class T> template <class BidirectionalIterator>


void replace (ForwardIterator first, void reverse (BidirectionalIterator first,
ForwardIterator last, BidirectionalIterator last)
const T& old_value, {
const T& new_value) while ((first!=last)&&(first!=--last))
{ {
while (first!=last) { std::iter_swap (first,last);
if (*first == old_value) *first=new_value; ++first;
++first; }
} }
}
Приклади

template< typename T > template< typename T >


void func3( T a[], int n ) void func4( T a[], int n, T x)
{ {
T t; for (int i = 0; i < n; i++)
for (int i = 0; i < n - 1; i++) {
for (int j = n - 1; j > i; j--) if (a[i] == x)
if (a[j] < a[j-1]) {
{ for (int j = i; j < n - 1; j++)
t = a[j]; {
a[j] = a[j-1]; a[j] = a[j + 1];
a[j-1] = t; }
} n--;
} }
}
}
Стандартні шаблони

template <class ForwardIterator, class T>


ForwardIterator remove (ForwardIterator first,
std::sort, ForwardIterator last,
std::stable_sort const T& val)
{
ForwardIterator result = first;
while (first!=last) {
if (!(*first == val)) {
*result = *first;
++result;
}
++first;
}
return result;
}
Приклади

template< typename T >


void func5( T a[], int n, int m)
{
int i = 0;
int j = m;
while (i != j)
{
T t = a[i];
a[i++] = a[j];
a[j++] = t;
if (j == n) j = m;
else if (i == m) m = j;
}
}
Стандартний шаблон

template <class ForwardIterator>


void rotate (ForwardIterator first,
ForwardIterator middle,
ForwardIterator last)
{
ForwardIterator next = middle;
while (first!=next)
{
swap (*first++,*next++);
if (next==last) next=middle;
else if (first==middle) middle=next;
}
}
Ще раз про Use Case Diagram та патерн Фасад
● Почати варто з визначення того, що окреслюється коло функціональності ПЗ, яке
проектується.

○ Використайте діаграму прецедентів (обов'язкове завдання ЛР1), опишіть “чернетку технічного завдання” своїми
словами. В ТЗ зазвичай окремо описують функціональні вимоги - що і як має робити програма. Візьміть за основу
коротке формулювання варіанту завдання і розширте його.

● Грубо кажучи, діаграма прецедентів описує те, що можуть користувачі (інші програмні
компоненти поки не чіпаємо) робити з допомогою конктретного ПЗ

○ В усіх варіаінтах є стандартні дії: завантажити налаштування, зберегти налаштування чи поточний стан у файл. Їх
треба передбачити, але не треба реалізовувати в ЛР1, оскільки будова класів ще зазнає змін.

○ Інші типові дії (прецеденти): запустити процес моделювання або гри або обробки зображення, отримати статистику,
додати нову подію в календар, знайти нотатку по ключовим словам, зробити хід у грі тощо.

○ В результаті, маємо набір дій, який формує набір методів класу-фасаду, через який відбувається взаємодія з
логікою ПЗ. Тобто: “прецедент Запуск симуляції” --> Sim.Run(), “прецедент Налаштування” -->
Sim.SetParams(params) тощо.

○ Фасад надалі оперуватиме іншими об'єктами, які будуть формувати логіку. Через фасад відбуватиметься взаємодія
з логікою чи з консолі, чи через графічний інтерфейс, чи через HTTP/REST/SOAP/будь-який-інший-протокол
Чи обов'язково реалізовувати MVC / MVVM / MVP і т.п.?
● Головна ідея: відокремити логіку від конкретного інтерфейсу користувача.

○ У вас це консоль ТА віконний інтерфейс, керований подіями. Принципи роботи з консоллю (зчитати дані з
блокуванням виконання) та з віконними подіями (обробити подію, що настала та пройшла крізь цикл обробки подій)
- відрізняються. Тому змішування консольного вводу-виводу в коді методів логіки є ПОГАНОЮ ПРАКТИКОЮ, що
призведе до багатьох змін в коді при написанні курсової. Слід розробити такий API, щоб зміна та отримання стану
системи не залежало від стилю роботи інтерфейсу користувача.

○ Найпростіше рішення - фасад. У вас буде незалежна від UI Модель (Фасад + інші класи логіки) та код UI. У випадку
консольного додатку можна не реалізовувати діалог з користувачем - просто набір викликів методів Фасаду з
конкретними параметрами (тестовий сценарій) та виведення на екран стану об'єктів. Якщо хочеться діалог - тоді
цикл зчитування вводу користувача та виконання відповідних дій через Фасад та інші об'єкти. В рамках курсової
(GUI) у Вас будуть обробники подій (натискання кнопок мишею тощо), в яких ви знову таки викликаєте методи того
самого Фасаду. При потребі “промалювати картинку” - дані вилучаються з Фасаду, як і для діагностичних
повідомлень в рамках ЛР.

○ В ідеалі, слід розділити також шар роботи з UI та правила реакції на дії користувача (контролер, презентер). Тоді
маємо 3 складові: M-V-P, M-V-C, M-V-VM тощо. Не наполоягаю.

○ Наполягаю: не використовуйте сторонніх бібліотек для GUI поки не готова “абстрактна” логіка програми. Досить
лише консолі. Навіть якщо у Вас “псевдо-real-time” задача, а не покрокова.

○ Див. також Unit Testing, Test-driven-development. В ЛР програма - більше набір тестів. В КР - вже має бути
інтерактивна програма.
Ще раз про SOLID SOLID Principles:
Single responsibility
Open–closed
Liskov substitution
● Принцип інверсії Interface segregation
залежностей є Dependency inversion
непоганим
прикладом для
використання.
● Реалізовуйте
залежності через
інтерфейси
Алгоритми
sort --> NlogN

introsort =
quicksort + heapsort + insertion sort
приклади

#include <iostream> // std::cout


#include <algorithm> // std::for_each
#include <vector> // std::vector

void myfunction (int i) { // function:


std::cout << ' ' << i;
}
std::cout << "myvector contains:";
struct myclass { for_each (myvector.begin(), myvector.end(), myfunction);
// function object type: std::cout << '\n';
void operator() (int i)
{std::cout << ' ' << i;} // or:
} myobject; std::cout << "myvector contains:";
for_each (myvector.begin(), myvector.end(), myobject);
int main () { std::cout << '\n';
std::vector<int> myvector;
myvector.push_back(10); return 0;
myvector.push_back(20); }
myvector.push_back(30);
приклади
#include <iostream> // std::cout
#include <algorithm> // std::copy_if, std::distance
#include <vector> // std::vector

int main () {
std::vector<int> foo = {25,15,5,-5,-15};
std::vector<int> bar (foo.size());

// copy only positive numbers:


auto it = std::copy_if (foo.begin(), foo.end(), bar.begin(), [](int i){return !(i<0);} );
bar.resize(std::distance(bar.begin(),it)); // shrink container to new size

std::cout << "bar contains:";


for (int& x: bar) std::cout << ' ' << x;
std::cout << '\n';

return 0;
}
Java Collections Framework

- Interfaces,
- Implementations,
- Algorithms

- Containers (incl. adapters,


iterators)
- Algorithms (with functors)

C++ Standard Template Library


приклади

List<String> list = new ArrayList<>(c); // c - інша колекція

Object[] a = c.toArray();

import java.util.*;

public class Shuffle {


public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (String a : args)
list.add(a);
Collections.shuffle(list, new Random());
System.out.println(list);
}
}
прохід колекцією, типові приклади

for-each:
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
pipeline: стиль оформлення коду,
roster не патерн pipeline
.stream()
.filter(e -> e.getGender() == Person.Sex.MALE)
.forEach(e -> System.out.println(e.getName()));

iterator:
for (Iterator<?> it = c.iterator(); it.hasNext(); ) {
if (it.next().getGender() == Person.Sex.MALE) {
System.out.println(it.next().getName());
}
}
pipeline як патерн
прохід колекцією, типові приклади

myShapesCollection.stream() видалення:
.filter(e -> e.getColor() == Color.RED) for (Iterator<?> it = c.iterator(); it.hasNext(); )
.forEach(e -> System.out.println(e.getName())); if (!cond(it.next()))
it.remove();
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
прохід колекцією, типові приклади

import java.util.*;
import java.util.stream.*;

public class FindDups {


public static void main(String[] args) {
Set<String> distinctWords = Arrays.asList(args).stream().collect(Collectors.toSet());
System.out.println(distinctWords.size()+ " distinct words: " + distinctWords);
}
}
------------------------------------------------------------------------
import java.util.*;

public class FindDups {


public static void main(String[] args) {
Set<String> s = new HashSet<String>();
for (String a : args)
s.add(a);
System.out.println(s.size() + " distinct words: " + s);
}
}
Алгоритми для списків

sort — sorts a List using a merge sort algorithm, which provides a fast, stable sort. (A stable
sort is one that does not reorder equal elements.)
shuffle — randomly permutes the elements in a List.
reverse — reverses the order of the elements in a List.
rotate — rotates all the elements in a List by a specified distance.
swap — swaps the elements at specified positions in a List.
replaceAll — replaces all occurrences of one specified value with another.
fill — overwrites every element in a List with the specified value.
copy — copies the source List into the destination List.
binarySearch — searches for an element in an ordered List using the binary search algorithm.
indexOfSubList — returns the index of the first sublist of one List that is equal to another.
lastIndexOfSubList — returns the index of the last sublist of one List that is equal to another.
Словники Multi-map??
Map<String, List<String>> m = new
import java.util.*; HashMap<String, List<String>>();

public class Freq {


public static void main(String[] args) {
Map<String, Integer> m = new HashMap<String, Integer>();

// Initialize frequency table from command line


for (String a : args) {
Integer freq = m.get(a);
m.put(a, (freq == null) ? 1 : freq + 1);
}

System.out.println(m.size() + " distinct words:");


System.out.println(m);
}
}
java Freq if it is to be it is up to me to delegate
8 distinct words:
{to=3, delegate=1, be=1, it=2, up=1, if=1, me=1, is=2}
8 distinct words:
{be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1} TreeMap
8 distinct words:
{if=1, it=2, is=2, to=3, be=1, up=1, me=1, delegate=1} LinkedHashMap
Словники
Компаратори

SortedMap<String, String> fileExtensions = new TreeMap<>(new Comparator<String>() {


@Override
public int compare(String s1, String s2) {
return s2.compareTo(s1); // чи щось цікавіше...
}
});
Знову про ефективність
Узагальнення

- Інтерфейс Collection є спільним для усіх колекцій крім словників


- методи forEach(), toArray(), iterator(), add(), contains(), isEmpty(), remove(), size(), stream() та ін.
- Set не дозволяє дублювати елементи
- HashSet на основі хешування, TreeSet - бінарного дерева, LinkedHashSet - хешування з
контролем порядку сортування
- List - типовий контейнер послідовностей з контролем порядку вставки та видалення
- ArrayList на відміну від Vector не синхронізований (менше накладних витрат (затримок)
при роботі, якщо це однопоточне виконання).
- LinkedList - класичний список, “в середньому по палаті” програє ArrayList в ефективності.
- Queue - для організації черг (FIFO) з додатковими методами вставки та доступу (poll(), peek()
та ін.)
- Deque - для вставки та видаленні на обох кінцях (FIFO/LIFO)
- Map - словники з унікальними ключами
- SortedMap - з підтримкою порядку сортування з використанням компараторів
- HashMap на основі хешування, TreeMap - бінарного дерева, LinkedHashMap -
хешування з контролем порядку сортування
- HashTable - синхронізований (повільніший) варіант HashMap, що не дозволяє нульових
ключів та значень.
Generic collections

class Program
{
static void Main()
{
int[] arr = { 0, 1, 2, 3, 4 };
List<int> list = new List<int>();

for (int x = 5; x < 10; x++)


{
list.Add(x);
}

ProcessItems<int>(arr);
ProcessItems<int>(list);
}
Non-generic collections (старий підхід)
static void ProcessItems<T>(IList<T> coll)
{
// IsReadOnly returns True for the array and False for the List.
System.Console.WriteLine
("IsReadOnly returns {0} for this collection.",
coll.IsReadOnly);

// The following statement causes a run-time exception for the


// array, but not for the List.
//coll.RemoveAt(4);

foreach (T item in coll)


{
System.Console.Write(item.ToString() + " ");
}
System.Console.WriteLine();
}
}
*http://people.cs.aau.dk/
Вимірюваний інженерний підхід: метрики в розробці ПЗ

 Кількість рядків коду: LOC, KLOC

 Кількість помилок на KLOC

 Часова складність: O(n), O(n2), O(n3), O(nlogn), O(logn), тощо


 Об'єм пам'яті: також О-нотація

 Цикломатична складність: кількість лінійно незалежних шляхів в алгоритмі роботи


програми (представленому за допомогою орієнтованого графа)

M = E − N + 2P

де

M - цикломатична складність,
E - кількість ребер в графі,
N - кількість вершин в графі,
P - кількість компонент зв'язності

для прикладу справа:

M = 10 − 9 + 2*1 = 3

а скільки насправді шляхів виконання слід передбачити в тестах?


Вимірюваний інженерний підхід: метрики в розробці ПЗ (2)

 Пов’язаність (згуртованість, зчеплення, cohesion):  Зв’язність (coupling): міра в якій модуль програми
якісна міра того, наскільки пов’язаним є код в одному залежить від кожного іншого модуля
модулі програми (від гіршої до кращої):
 якісно (від сильної до слабкої):
 випадкова
 логічна (спільне призначення)  за вмістом
темпоральна (часова, спільний час виклику) процедурна

(спільна послідовність дій)


 спільні глобальні дані
 комунікаційна (спільні дані)  зовнішній інтерфейс
 послідовна (спільний конвейєр виконання)  контроль
 функціональна (спільне завдання)  фрагменти складних струк тур
даних
 структури даних
 повідомлення
 відсутня

 кількісно для модуля:


C=1–1/S

де S – сума кількості вхідних та вихідних параметрів


керування та даних, глобальних змінних, модулів,
що викликають модуль та викликаються ним.

https://ru.wikipedia.org
SRP.OCP.LSP.ISP.DIP

High-level modules should not depend on low-level modules.


Both should depend on abstractions (e.g. interfaces).

Abstractions should not depend on details. Details (concrete


Dependency inversion (принцип) implementations) should depend on abstractions.

Inversion of control

Dependency injection
- Library vs. Framework, в чому різниця?

Dependency inversion (принцип)

Inversion of control (ідея)

Dependency injection

- Хто контролює об'єкт (керує ним)?


constructor injection
The dependencies are provided through a
client's class constructor.
setter injection
The client exposes a setter method that the
injector uses to inject the dependency.
interface injection
The dependency's interface provides an
injector method that will inject the
dependency into any client passed to it.
Clients must implement an interface that
exposes a setter method that accepts the
dependency.

Dependency inversion (принцип)

Inversion of control (ідея)

Dependency injection (механізм)


Strategy / Стратегія

* Клієнт абстрагується від процесу виконання


конкретних дій

= схоже на ідею, закладену у функтор

+ для зміни фактичних дій достатньо передати


клієнту інший об'єкт-стратегію, код клієнта
незмінний

* інверсія управління в тому, що клієнт не викликає


конкретні дії ані напряму, ані через пряму
залежність від класів-стратегій.

Відповідно, незмінний клієнт можна розширити


новою поведінкою, лише створивши нову стратегію
та передавши її клієнту під час виконання, а не
компіляції.
Template method / Шаблонний метод

* Клієнт абстрагується від ОКРЕМИХ КРОКІВ


процесу виконання конкретних дій

= дуже схоже на стратегію

* відмінність: стратегія визначає алгоритм вцілому,


а шаблонний метод - це лише один з варійованих
кроків незмінного алгоритму

* може використовуватися у UI фреймворках таким


чином: базовий абстрактний клас закладає основи
поведінки UI, але залишає невизначеними дії при
настанні певних подій. Для конкретної програми
створюється новий клас, що наслідує базовий, а
шаблонні методи (“обробники подій”) є місцем
опису логіки реакцій на настання подій.
Factory method / Фабричний метод

* Клієнт абстрагується від процесу створення


об'єктів.

Не він контролює, об'єкти яких класів будуть


створені == він не залежить від цих класів

+ для зміни типу створюваних об'єктів


достатньо передати клієнту іншу фабрику,
код клієнта незмінний

= при додаванні нового під-типу продукту додається


і нова фабрика

= фактично, це стратегія створення об'єктів


Abstract Factory / Абстрактна фабрика

* Клієнт абстрагується не лише від


процесу створення об'єктів, а й від того,
які під-типи об'єктів будуть створені

* Підходить для створення груп (сімейств)


пов'язаних об'єктів

+ для зміни сімейства створюваних об'єктів


достатньо змінити фабрику

= при додаванні нового сімейства додається і


нова фабрика

- при додаванні нового “продукту” його


потрібно додати в кожне сімейство та додати
методи у всі фабрики
Service Locator pattern
Продовження GoF патернів
F D D

D F

D F функція як об'єкт

D делегування

D
Функція як об'єкт

Основна ідея:

- якщо функціональність змінюється через


перевизначення методів (як в Шаблонному Методі),
то ці зміни застосовуються статично під час
компіляції

- якщо функціональність оформлена як об'єкт, то


зміни функціональності можливі динамічно під час
виконання програми (з відомих нам прикладів -
Стратегія)
Делегування. Заміна наслідування делегуванням (композицією)

Якщо функціональність, яку потрібно повторно


використати, оформлена у деякому класі,
то сильніша залежність типу наслідування може
бути замінена слабшою залежністю типу композиція,
особливо, якщо клас-споживач функціональності не
є коректним нащадком класу-постачальника
функціональності (тобто замість відношення “є”
насправді маємо відношення “частково подібний”
або “використовує”)

При делегуванні начебто доводиться “дублювати”


чи “засмічувати” код - прописувати делегуючі
виклики в тих самих методах. Однак це прийнятні
жертви за те, що логіка описується лише раз, а
також за слабший зв'язок між класами
Bridge / Місток

Відомі подібні приклади ідєї: ідіома PIMPL

* Основна ідея - відокремити абстракцію від


реалізації, тобто уможливити окремі ієрархіі
абстракцій та реалізацій

* Технічно - це та сама Стратегія

* Але це структурний шаблон, тобто його ідея -


ідіома PIMPL розділенні ієрархії класів на гілку абстракцій та гілку
реалізацій.
* Основна ідея - інкапсулювати всю інформацію для
виконання дій у потрібний час (за розкладом, по
Command / Команда настанню події).

* “Той, хто викликає” команду, не залежить від


отримувача команди

* Можна створювати кілька типів команд, що


працюють з одним отримувачем

* Або можна мати кілька об'єктів, що викликають ті


самі команди

* Можна використовувати для обліку виконаних


команд та їх відміни (Undo, транзакції)
Проксі (заміщувач) / Proxy

* Ідея: реалізувати додатковий контроль за


використанням реального об'єкта через об'єкт-
проксі

* Контролювати, серед іншого можна процес


створення реального об'єкта (створюючи його лише
за потреби)

* Або може бути потрібно виконати певний


додатковий набір дій при роботі з реальним
об'єктом
Адаптер / Adapter

* Приклад ідеї: контейнери-адаптори STL

* Основна відмінність патерну від інших:


перетворення наявного інтерфейсу на той, що
потрібен
Декоратор / Decorator
* Основна ідея: уникнути зайвого розростання
ієрархії класів лише для того, щоб виконати певні
додаткові дії

* Важливо: на відміну від Адаптера, Декоратор не


змінює інтерфейс

Component c = new BorderedWindow(new VerticallyScrolledWindow(new Window(1,2,10,10)));


Компонувальник / Composite

* Основна ідея: дозволити працювати зі об'єктами,


складеними з інших об'єктів, так само, як і з
простими об'єктами

* Корисно для організації роботи з деревовидними


ієрархіями об'єктів

* Композит делегує виконання своїм компонентам


Composite: приклад з минулої лекції

int main()
{
PaintablesFactory *f = new PaintedFactory(new ConsolePainter());
... std::vector<Point> v = { Point(0,0), Point(1,1), Point(1,0) };
Paintable *p = f->create(ShapeName::Triangle, v);
class ShapeGroup : public Paintable ...
{
std::vector<Paintable*> groupedShapes; ShapeGroup grp;
public: grp.add(p);
void add(Paintable* p)
{ ShapeGroup grp2;
groupedShapes.push_back(p); grp2.add(p);
} grp2.add(&grp);
void paint() grp2.paint();
{
for (int i = 0; i < groupedShapes.size(); ++i) grp.add(&grp);
groupedShapes[i]->paint(); // infinite loop: grp.paint();
} // we need extra check on adding itself
};
return 0;
... }
Продовження GoF патернів
Будівельник / Builder

● створення складеного об'єкта за


кілька кроків (наперед невідома
їх кількість та порядок)
● відокремлення коду зі створення
(конструювання) об'єктів від їх
представлення: в результаті
можна створювати об'єкти, різні
за будовою та представленням
● “багатокрокова фабрика”
Прототип / Prototype

● як і фабрика, дозволяє уникнути new в коді


● на відміну від абстрактної фабрики, немає потреби у ієрархії
наслідування фабричних класів
● мабуть, найбільш важливо: коли робота з об'єктом йде через
інтерфейс (“вказівник чи посилання на базовий клас або
інтерфейс”), клієнт не знає, з об'єктом якого саме класу він
працює в конкретний момент, і який метод фабрики викликати.
Але сам об'єкт має всю інформацію, щоб створити свою копію
(того ж самого класу).
● На відміну від фабрики потрібна початкова ініціалізація об'єкту.
● фабрика та прототип не виключають один одного: фабрика
може бути реалізована через клонування набору прототипів, а
прототип теоретично може викликати фабричні методи
● Композит добре суміщається з прототипом
Ланцюжок відповідальності /
Chain of responsibility

● для обробки “запиту” більш ніж одним


обробником
● обробники передають запит далі по
ланцюжку, якщо не можуть його
обробити
● тобто, побудова ланцюжка (між
if (canHandle(req)) {
об'єктами-обробниками, під час Handle(req)
виконання коду) визначає потік } else {
successor.HandleRequest(req)
керування, схожий на if-else-if. }
Спостерігач /
Observer
● для незалежної обробки “подій”
довільною кількістю обробників
● інші назви: “підписка на оповіщення”,
“видавник та підписники“
● генератор інформації (постачальник
оповіщень) інформує усіх
зареєстрованих слухачів (споживачів
оповіщень) про настання події, а
споживачі в разі потреби можуть
звернутися до постачальника за
даними по його поточному стану
● подієво-орієнтований підхід, поширений
як у програмах з GUI, так і без нього
Посередник /
Mediator
● для організації опосередкованої
взаємодії між об'єктами
● об'єкти, що взаємодіють, не знають
один про одного, ці зв'язки динамічно
формуються у медіаторі
● медіатор, а не об'єкти - генератори
подій, є диспетчером, що керує потоком
керування
Відвідувач / Visitor

● для привнесення в усталену ієрархію


класів нових дій

○ при додаванні нових методів до інтерфейсу


ієрархії це призвело б до суттєвої зміни всієї
ієрархії класів

○ можливе порушення SRP діаграма послідовності


● можливе додавання будь-якої кількості
нових дій над об'єктами ієрархії, якщо Client ElementOne VisitorTwo
ієрархія підтримує механізм “відвідин” accept(v: VisitorTwo)
● Подвійна диспетчеризація
v.visit(e: ElementOne)
● Якщо ієрархія не усталена, то
додавання нових класів до неї призведе
до змін у всіх ієрархіях відвідувачів
Завершення GoF патернів
Легковаговик, пристосуванець / Flyweight

● Для роботи з великою кількістю об'єктів,


що не є повністю унікальними
● Викокремлюється частина стану об'єкта,
що є спільною для багатьох
екземплярів (внутрішній стан). Таким
чином, кількість реально створених
об'єктів суттєво скорочується. Інша
частина стану, що є унікальною для
екземплярів, передається до створених
об'єктів та (повертається з них) через
параметри операцій.
● Приклади: зображення літер у шрифтах
(зображення - внутрішній стан, позиція
на екрані - зовнішній стан), набір іконок
для GUI, бойові юніти в RTS, будівлі та
дороги у місті, взагалі будь-які типовІ,
шаблонні сутності, придатні для
повторного використання.
Інтерпретатор / Interpreter

● Для організації обробки власної мови команд


(little / small language)
● Кожна мова повинна мати свою чітку семантику
та синтаксичні правила. По синтаксичним
правилам будується абстрактне дерево
синтаксису (AST), що представляється окремими
об'єктами-виразами, поєднаними відношенням
композиції. Всі вирази реалізують спільний
інтерфейс, а семантика різних типів виразів
закладена в реалізації спільного методу
“інтерпретувати”.
● Поле використання - не лише “власна екзотична
мова програмування”: “калькулятор” з
формулами (табличний редактор), виконання
бізнес-процесів на певній мові типу WS-BPEL,
скриптові мови для опису легко змінної логіки
поведінки об'єктів та багато іншого.
BPMN - Business Process Modeling Notation
Ітератор / Iterator

● Ідея: розділити колекції та процедуру їх обходу


● Пересування колекцією здійснюється через інший
клас, об'єкти якого мають доступ до даних
колекції
● Колекція реалізує лише інтерфейс створення
ітераторів
● Ітератори можуть реалізовувати невелику
кількість операцій їх спільного інтерфейсу для
різних колекцій, таким чином код, що залежить
від інтерфейсів ітераторів, абстрагується від
конкретики зберігання даних в різних колекціях.
● Проблемним питанням є організація доступу до
даних лише відповідних ітераторів. Колекція,
фактично, має бути тісно пов'язана з ітератором.
Хранитель / Memento

● Ідея: відокремити сутність зі “знімком” стану


об'єкта для подальшого можливого повернення
об'єкта до збереженого стану
● Застосовують також тоді, коли наявність повного
відкритого інтерфейсу, достатнього для
збереження стану, порушує інкапсуляцію (дуже
часто) або занадто сильно розкриває деталі
реалізації.
● Схоже на ітератор тим, що хранитель стану, як і
ітератор, тісно пов'язаний з класом об'єкту, який
він обслуговує.
● Часткова альтернатива патерну команда по
управлінню станом. Команди “містять” набір дій з
переведення об'єкта у інший стан, а хранитель
зберігає сам стан об'єкта.
Стан / State
● Для реалізації можливості зміни поведінки об'єкта під час
виконання програми залежно від поточного “стану” (в
загальноприйнятому розумінні) об'єкта
● Зручний для реалізації моделі “кінцевих автоматів” (state
machine)
● Ті самі дії в різних станах призводять до різних наслідків:
стан редагування та стан перегляду, стан створення та
стан готовності до використання, стан ходу певного гравця,
тимчасові модифікації поведінки юнітів тощо.
● Об'єкт-стан може змінювати себе на інший об'єкт-стан при
виконання делегованих йому дій.
Про архітектурні патерни...
Від простого до складного...

IoC (“Don't Call Us, We'll Call You”)


Принципи проектування

DRY (don't repeat yourself) vs. WET (write everything twice)


● Загальний принцип, застосовний і
до схем БД, і до планів тестування, і
до документування тощо - будь-
якого “фрагмента знань”: він має
бути унікальним без необхідності YAGNI (you aren't gonna need it)
його визначати кілька разів.
● Прикладом можуть бути навіть ● Не додавайте функціональність “на майбутнє”, яка
однакові написи на елементах GUI явно не потрібна тут та зараз
(ярлик, підказка на різних ○ Пов'язані поняття:
елементах тої самої дії)
■ Worse is Better: з т.з. практичності використання та якості ПЗ,
● Фреймворки - спосіб реалізації для простота реалізації (“чим гірше” програма, тобто менше
уникнення опису повторюваних можливостей у неї) та (вторинно) простота інтерфейсу
елементів (коду, даних..). краще за багату функціональність, якою складно
користуватись і яку складно підтримувати
● Також автоматичні генератори коду,
системи збірки, скриптові мови. ■ MVP (minimal viable product) - мінімальний життєздатний
продукт (на противагу дорогій тривалій повноцінній
реалізації продукту з широкою функціональністю, що може
не виправдати очікувань)
Предметно-орієнтоване проектування / Domain-driven design

● Концентрація основної уваги на (складній) предметній


області. Тобто, знання про предметну область керують
проектуванням ПЗ Зв'язок з іншими ідеями:
● Основні концепти: домен (пр.обл.), модель (с-ма абстракцій,
- Model-driven engineering / architecture:
що описує домен), контекст (умови, що визначають MDE фокусується на способах
конкретне значення чогось), універсальна мова (що автоматизованого впровадження моделей
в кінцевий код
використовується всіма в команді)
● Будівельні блоки: - POJO / POCO (Plain old Java/CLR objects):
об'єкти, що не прив'язані до конкретної
○ сутності (їх атрибути не відіграють великого значення) та value objects “технології” чи фреймворку, просто
(не мають концептуальної ідентичності, сукупність атрибутів. В описують предметну область
контексті громадян банкноти - об'єкти значення (номінал), а для
- naked objects: GUI має бути прямим
нацбанку - сутності (облік кожної банкноти)) представленням доменних об'єктів (тобто
модель домена має бути достатньо
○ сукупність (агрегат), сервіс, сховище, фабрика, подія продуманою для цього)
Чиста архітектура / Clean Architecture (Robert C. Martin)
ОКРЕМІ ЕЛЕМЕНТИ

Структура організації обчислень, даних :


- розподілена ієрархічна
- конвейєр

Master-slave pattern Pipe / filter pattern

*https://towardsdatascience.com/10-common-software-architectural-patterns-in-a-nutshell-a0b47a1e9013
Розподілені обчислення

“Багатошаровість” :) / Layered pattern Клієнт-сервер / Client-server


Клієнт2 Клієнт3

Триланкова архітектура /
3 (multi) - tiered:
- Клієнт
- Сервер бізнес-логіки
- БД

* у кожного шару свої обов'язки


* слід намагатися уникнути створення “монолітного” (нерозподіленого) ПЗ
Шина подій /
Одноранговість / Event bus
Брокер / Broker
Peer-2-peer

“пубілкація-підписка”
*https://dzone.com/articles/software-architecture-the-5-patterns-you-need-to-k
Сервісно-орієнтована архітектура

Мікросервіси / Microservices

DB

DB
Сервісна шина /
Enterprice Service CRUD DB

Bus
DB

--> “Cloud computing” (*-as-a-Service)


--> “Serverless architecture” (FaaS)

*https://dzone.com/articles/software-architecture-the-5-patterns-you-need-to-k
Модельно-орієнтована архітектура

You might also like