Про модель, логіку, ООП, розробку та інше

Чи Часто ви замислюєтеся – чому щось зроблено так чи інакше? Чому у вас микросервисы або моноліт, двухзвенка або трехзвенка? Навіщо вам багатошарова архітектура і скільки у вас взагалі шарів? Що таке бізнес-логіка, логіка програми, презентаційна логіка і чому все так розділене? Подивіться на свою програму – як воно взагалі спроектовано? Що в ньому і де знаходиться, чому це зроблено саме так?
Тому що так написано в книжках чи так говорять авторитетні особистості? Які ВАШІ проблеми вирішує той чи інший підхід/патерн?
Навіть те, що на перший погляд здається очевидним, часом буває дуже складно пояснити. А іноді, у спробі пояснення, приходить розуміння того, що очевидні думки були і зовсім помилкові.
Давайте спробуємо взяти якийсь приклад і вивчити на ньому ці питання з усіх боків.

Іграшкове місто
Віртуальний місто
SOLID як багато в цьому слові ...
Предметна область
Презентаційна логіка
Збереження стану
Багатошаровість
2-tier
N-tier
2 як 3
Сервіси
Інструменти
Теорія та практика
Підсумок

Іграшкове місто
Давайте уявимо невеликий іграшкове місто. Він складається з ряду будов, через нього проходить кілька доріг. По дорогах переміщуються машини і ходять люди. Рух регулюють світлофори. Все, що відбувається в місті підпорядковане певним правилам і всім цим різноманіттям можна управляти.

Людей і машини можна переміщати, перемикати світлофори, змінювати час дня і ночі і т. п… З цим містом одночасно можуть взаємодіяти кілька людей. Вони можуть або просто спостерігати, або щось робити, змушуючи місто змінюватися. Все це прекрасно існує, але настає момент, коли з'являється необхідність перенести іграшкове місто у віртуальний світ.


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

Віртуальний місто
Для початку потрібно зробити найголовніше – створити модель нашого віртуального міста. Хоч це може здатися чимось простим, насправді, в цьому криється більшість проблем і складнощів. Але починати все одно треба, так що приступимо.

Наша мета – описати модель міста у віртуальному вигляді. Для цього ми візьмемо будь-який популярний об'єктно-орієнтований мова високого рівня. Використання такої мови передбачає використання об'єктів як основні цеглинок для створення віртуальної моделі міста.

Звичайно, можна просто описати всю модель одному об'єкті, але це загрожує лише зайвою складністю і заплутаністю. Коли всі «звалене» в одне місце і незрозуміло як перемішано, стає важко розуміти, що взагалі відбувається і тим більше вносити які-небудь зміни. Тому, щоб зробити простіше і не заплутатися в отриманій програмі, ми розіб'ємо опис нашого міста на окремі невеликі частини.

В якості таких частин ми візьмемо те, що легко відокремлюється один від одного при погляді на наш реальний місто — окремі об'єкти цього міста (будинок культури, червона БМВ на перехресті, Петрович, той, що біжить по своїх справах). Опис кожного об'єкта у віртуальному світі являє собою опис його властивостей (колір, модель, назва, розташування тощо). Щоб не повторюватися і не описувати однакові властивості для схожих об'єктів кожен раз, ми виділимо групу таких властивостей і назвемо їх типом об'єкта. Хорошими кандидатами є такі загальні типи, як машина, дім, людина і т. п. Вони дозволять зосередити в собі опис основних властивостей. А різні види машин, наприклад, будуть доповнювати базовий тип «машина» своїм унікальним набором властивостей, створюючи цілий набір нових типів. Такі нові типи по відношенню до вихідних типів називаються спадкоємцями. А сам процес створення нового типу, на основі існуючого — спадкуванням.


Всі створені нами типи об'єктів будуть представляти модель.

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

І ось начебто все розставлено по своїх місцях, група машин стоїть на перехресті в очікуванні зеленого сигналу світлофора, дівчинка Юля — в очікуванні свого ліфта, і навіть вода застигла в трубах величезного хмарочоса. Ми наповнили нашу модель станом, повторивши стан нашого реального міста за якийсь певний момент часу.

Але, якщо ми уважно подивимося на наш реальний місто, то ми побачимо, що він постійно змінюється. Всі зміни його стану являють собою зміни значення властивостей різних об'єктів, поява нових об'єктів або зникнення старих. Ось світлофор переключився і поміняв значення властивості «Поточний сигнал світлофора» червоного зелений. Ось ліфт поміняв значення властивості «Поверх» другого перший і значення властивості «Відкриті двері» так ні.

Значить для того щоб наше місто ожив, наша програма повинна вміти змінювати стан моделі, а це означає — вміти змінювати властивості різних об'єктів, додавати нові об'єкти або видаляти старі, тобто володіти поведінкою.

Для цього додамо в програму всі можливі дії у нашому місті. Кожну таку дію можна описати у вигляді процедури зміни властивостей якого-небудь об'єкта або групи об'єктів. Після опису всіх цих процедур стає видно, що їх кількість досить велика. Щоб спростити підтримку і зміни всіх доступних процедур їх слід розбити на групи. Недовго думаючи, можна згрупувати такі процедури по однотипності їх дій, отримавши таким чином набір класів, кожен з яких буде відповідати за набір схожих дій.

Начебто, тепер все поділено і виглядає дуже навіть непогано, але є одне «але». Опис властивостей наших об'єктів повністю відокремлене від процедур, які ці властивості змінюють і перетворюють нашу модель анемічну модель. При такому поділі абсолютно незрозуміло, як можуть змінюватися властивості того чи іншого об'єкта. А якщо змінити властивості одного повинно бути пов'язано із зміною інших властивостей або залежить від значення інших властивостей, то це знання про внутрішній устрій об'єкта доведеться продублювати у всіх процедурах, що змінюють цю властивість. Щоб уникнути цих проблем ми не будемо групувати процедури по діям, а розкладемо ці процедури з тим типам, властивості яких вони міняють.

Завдяки цьому, для публічного доступу можна буде залишити тільки використовувані іншими властивості і методи, приховавши знання про внутрішній устрій об'єкта. Такий процес приховування внутрішніх принципів роботи називається інкапсуляція. Наприклад, ми хочемо перемістити ліфт на кілька поверхів. Для цього потрібно перевірити стан дверей – відкриті або закриті, запустити і зупинити двигун і т. п. Вся ця логіка буде просто приховано за дією «переміститися на поверх». У підсумку виходить, що тип об'єкта представляє собою набір властивостей і процедур, які змінюють ці властивості.

Деякі процедури можуть мати однаковий зміст, але бути пов'язаними з різними об'єктами. Наприклад, процедура «видати звуковий сигнал» є і у червоній BMW і у синіх жигулів. І хоч всередині вони можуть бути виконані абсолютно по-різному, вони несуть в собі один і той же зміст.

Так як у нас вже є загальний тип «машина», ми можемо помістити процедуру «видати звуковий сигнал» туди. Якщо логіка такої поведінки у всіх одна, то там же її можна буде визначити. Це спростить похідні типи і позбавить від дублювання коду. Але якщо раптом, в похідних типах логіка такої поведінки буде відрізнятися, то її можна легко змінити за допомогою зміни поведінки. Така можливість називається поліморфізмом.

Абстракція, наслідування, інкапсуляція і поліморфізм – це ті принади ООП, слідуючи яким можна створювати більш гнучкий дизайн. На додачу до них існує певний набір принципів, мета яких така ж – допомогти створити більш гнучкий дизайн.

SOLID як багато в цьому слові ...
SOLID – абревіатура загальновідомих принципів об'єктно-орієнтованого дизайну. В цій абревіатурі їх приховано п'ять, по одному на кожну букву.

Single Responsibility Principle (принцип єдиної відповідальності) — у кожного об'єкта повинна бути тільки одна причина для зміни.

Для того, щоб у кожній кімнаті кожного будинку можна було використовувати і інтернет, і електрика — була створена єдина розетка з роз'ємами для інтернет-проводи та електричного проводу.

public class Socket 
{
private PowerWire _powerWire;
private EnthernetWire _enthernetWire;

public Socket(PowerWire electricWire, EnthernetWire enthernetWire)
{
_powerWire = powerWire; 
_enthernetWire = enthernetWire;
}
...
}

Для роботи такої розетки необхідно було підводити інтернет-провід електричний дріт. І, здавалося б, немає нічого страшного в тому, що не у всіх розетках завжди використовується і інтернет, і електрика, зате компактно і зручно. Але все стає не таким зручним, коли настає пора змін.

Спочатку з'явилися квартири і навіть цілі будинки, в яких був не потрібен інтернет. Зовсім. Але для того, щоб наші розетки могли в них працювати, довелося протягувати інтернет-дроти і туди, що тільки збільшило вартість робіт, не принісши ніякої користі.

Потім з'явилася вимога, що розетки повинні бути обладнані додатковим кабелем для заземлення і з-за цього довелося міняти ВСІ розетки, в тому числі і ті, які використовувалися тільки для інтернету. Був пророблений великий обсяг роботи, який міг би бути менше, якби не порушувалися розетки, використовувані тільки для інтернету.

public class Socket 
{
private PowerWire _powerWire;
private EnthernetWire _enthernetWire;
privte GroundWire _groundWire;

public Socket(PowerWire powerWire, EnthernetWire enthernetWire, GroundWire groundWire)
{
_powerWire = powerWire;
_enthernetWire = enthernetWire;
_groundWire = groundWire;
}
...
}

Але останньою краплею стала вимога — поміняти для всіх інтернет-розеток інтернет-провід на новий стандарт. А так як взагалі всі розетки є одночасно ще й інтернет-розетками – то довелося знову міняти ВСЕ. Хоча обсяг робіт міг би бути набагато менше, так як кількість розеток, використовуваних для інтернету, в рази менше кількості всіх розеток.

public class Socket 
{
private PowerWire _powerWire;
private SuperEnthernetWire _superEnthernetWire;
privte GroundWire _groundWire;

public Socket(PowerWire powerWire, SuperEnthernetWire enthernetWire, GroundWire groundWare)
{
_electricWire = electircWire;
_superEnthernetWire = superEnthernetWire;
_groundWare = groundWare;
}
...
}

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

Щоб уникнути таких проблем — розетка повинна була бути розділена на дві незалежні одна від одної частини – електричну розетку і інтернет-розетку:

public ElectricSocket 
{
private ElectircWire _electricWire;
privte GroundWare _groundWare;

public ElectricSocket(ElectircWire electricWire, GroundWare groundWare)
{
_electricWire = electircWire;
_groundWare = groundWare;
}
...
}

public class InternetSocket 
{
private SuperEnthernetWire _superEnthernetWire;

public Socket(SuperEnthernetWire enthernetWire)
{
_superEnthernetWire = superEnthernetWire;
} 
...
}

А в тих випадках, коли дійсно була б потрібна і електрична та інтернет-розетка можна було б використовувати агрегацію:

public ElectricInternetSocket 
{
private ElectricSocket _electricSocket;
privte InternetSocket _internetSocket;

public ElectricInternetSocket (ElectricSocket electricSocket, InternetSocket InternetSocket)
{
_electricSocket = electircWire;
_internetSocket = groundWare;
}
...
}

Open-closed principle (принцип відкритості-закритості) – об'єкти повинні бути закриті для модифікацій, але відкриті для розширень.

Для оповіщення людей про важливої інформації в центрі міста, на самому жвавому перехресті був встановлений великий екран. На ньому відображався текст повідомлень, що приходять з різних джерел.

public class Message 
{
public string Text {get;set;}
}

public class BigDisplay
{
public void DisplayMessage(Message message)
{
PrintText(message.Text);
}
}

Через якийсь час з'явився новий вид повідомлень — повідомлення, що містять дату. І для таких повідомлень на екрані було необхідно відображати дату і текст. Доопрацювання можна було виконати різними способами.
1. Створити новий тип, похідний від типу повідомлення, додати йому атрибут «дата» і поміняти процедуру відображення повідомлень.

public class Message 
{
public string Text {get;set;}
}
public class MessageWithDate : Message
{
public DateTime Date {get; set;}
}
...
public void DisplayMessage(Message message)
{
If (message is MessageWithDate)
PrintText(message.Date + Message.Text)
else
PrintText(message.Text);
}

Але такий спосіб поганий тим, що доведеться змінювати поведінку всіх типів, які яким-небудь чином виводять повідомлення. І якщо в майбутньому з'явиться ще якийсь новий, особливий тип повідомлень — все доведеться міняти ще раз.

2.Додати властивість «дата» тип «повідомлення» і поміняти спосіб отримання тексту, щоб вийшло так:

public class Message 
{
private string _text;
public string Text 
{
get 
{
If(Date.HasValue)
return Date.Value + _text;
else
return _text;
}
set { _text = value; }
}
public DateTime? Date {get;set;}
}

Але, по-перше, такий спосіб поганий тим, що нам доводиться міняти основний тип і додавати в нього поведінку, яке властиве не всім повідомленнями, з-за чого з'являються зайві перевірки. По-друге, при появі нового типу повідомлення нам доведеться створювати ще один атрибут, який буде не у всіх повідомлень, і додавати зайві перевірки в код. По-третє, тепер відсутній спосіб отримання тексту повідомлення без дати для повідомлень з датами. І в разі такої необхідності — текст повідомлення доведеться виколупувати.

3. Відразу записувати дату в текст повідомлення, щоб взагалі не створювати ніякого нового типу і не змінювати спосіб відображення повідомлення на екрані. Але такий спосіб поганий тим, що в тих місцях, де від повідомлення потрібно буде отримати тільки дату — спочатку доведеться визначати, чи містить це повідомлення дату взагалі, а потім виокремлювати її з тексту повідомлення.

Якщо слідувати принципу відкритості-закритості, можна уникнути всіх цих проблем і піти четвертим шляхом:

public class Message 
{
public string Text {get;set;}
public virtual string GetDisplayText()
{
return Text;
}
}
public class MessageWithDate : Message
{
public DateTime Date {get; set;}
public override string GetDisplayText()
{
return Date + Text;
}
}
...
public void DisplayMessage(Message message)
{
PrintText(message.GetDisplayText());
}

При такому підході нам не потрібно змінювати базовий тип «повідомлення» — він буде залишатися закритим. У той же час він буде відкритим для розширення його можливостей. Крім цього, пропадуть всі проблеми, властиві іншим підходам.

Liskov substitution principle (принцип підстановки Барбари Лисков) — функції, які використовують базовий тип, повинні мати можливість використовувати підтипи базового типу, не знаючи про це.

Після додавання в програму типу «велосипед» настав час додати ще один тип – «мопед». Мопед — він же як велосипед, тільки ще краще. Значить велосипед відмінно підійде в якості базового типу для мопеда. Сказано-зроблено, і в програмі з'явився ще один тип «мопед» — похідний від типу «велосипед».

public class Bicycle 
{
public int Location;
Public void Move (int distance) {}
}

public class MotorBicycle : Bicycle 
{
public int Fuel;
public override void Move(int distance) 
{
If (fuel == 0) 
return;
...
fuel = fuel – usedFuel;
}
}

Але порівняно з велосипедом, у мопеда є одна неприємна особливість – якщо бензин закінчується, то мопед вже не може рухатися. І цю неприємну особливість довелося врахувати в коді. Врахували, а значення надавати не стали – на те у нас і похідний тип, щоб враховувати всякі специфічні особливості.

Так як мопед швидше ніж велосипед, то при можливості він став використовуватися в програмі там, де раніше використовувався велосипед. Але, в один прекрасний день програма зависла намертво. Пошук помилки був довгий і болісний, т. к. проблема періодично повторювалася і випадковим чином. Опустимо опис безсонних ночей і відразу перейдемо до винуватця всіх бід – методу, який переміщував велосипедиста між двома, далеко розташованими один від одного крапками:

public void LongJourney (int to, Biker biker, Bicycle bicycle)
{ 
while(bicycle.location < to)
{
Int distance = to - bicycle.location;
If (distance > 1000)
distance = 1000;
bicycle.move(distance);
biker.sleep();
}
}

Коли метод замість велосипеда передавався мопед з недостатньою кількістю бензину, він зависав назавжди з-за того, що мопед не просувався ні на сантиметр, навіть якщо така дія викликали явно. Щоб виправити цю проблему можна було б зробити так, звичайно:

public void LongJourney (int to, Biker biker, Bicycle bicycle)
{ 
while(bicycle.location < to)
{
Int distance = to - bicycle.location;
If (distance > 1000)
distance = 1000;
if (bicycle is MotorBicycle && bicycle.Fuel == 0)
FillFuel(bicycle);
bicycle.move(distance);
biker.sleep();
}
}

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

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

Interface segregation principle (принцип поділу інтерфейсів) — клієнти не повинні залежати від методів, які вони не використовують.

Для додання більшої реалістичності було додано ще одне цікаве поведінку. Якщо в недозволеному місці, на дорозі, з'являвся чоловік, то транспортний засіб, яким він завадив — повинно було зупинитися і посигналити. Для цього був реалізований метод:

public void CheckIntersect(Car car, People[] people)
{
...
If (car intersect people)
{
car.Stop();
car.Beep();
}
}

Поведінка було перевірено і все було добре до тих пір, поки в системі не з'явився велосипед і не збив людину, який вибіг на шосе. І це не дивно, адже перевірку зіткнення і автоматичну зупинку зробили тільки для автомобілів.

Першої ж божевільною ідеєю могло бути бажання стати зробити велосипед похідним від типом типу «машина», щоб його можна було легко підставляти в такі методи, без їх зміни. Адже якщо придивитися уважніше, в процедурі використовуються тільки такі дії машини, які є і у велосипеда, і нічого страшного не станеться. Але це тільки в цьому методі. Якщо такий дивний похідний тип передати в яку-небудь іншу процедуру, яка що-небудь машинно-специфічне, то така процедура обов'язково зламається.

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

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

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

public interface IVehicle 
{
...
void Stop();
void Beep();
...
}
public class Car : IVehicle { ... }
public class Bycycle : IVehicle { ... }
public void CheckIntersect(IVehicle vehicle, People[] peoples)
{
...
If (vehicle intersect peoples)
{
vehicle.Stop();
vehicle.Beep();
}
}

Тоді, в такий метод можна буде передати будь-який існуючий вид транспортного засобу і будь-який вид, який з'явиться в подальшому, при дотриманні ним умов контракту.

Dependency Inversion Principle (принцип інверсії залежностей) — абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.

У нашому місті існує система оповіщення новин. Система отримує важливі новини і транслює їх через систему динаміків міста.

public class NotifySystem
{
private SpeakerSystem _speakerSystem = new SpeakerSystem();

public void NotifyOnWarning(string message)
{
_speakerSystem.Play(message);
}
}

Все працює чудово, але вимогам властиво змінюватися. Тепер ми повинні іноді передавати повідомлення не через систему динаміків, а через мобільні телефони, відправляючи їх у вигляді смс. Звичайно, ми можемо зробити ще один об'єкт для відправки повідомлень про повідомлення через смс, але для цього доведеться дублювати більшу частину логіки. Щоб цього уникнути — ми підемо іншим шляхом. Системи оповіщення, в принципі, все одно як її повідомлення будуть відображатися. Найголовніше – це їх відправлення. Тому, ми можемо зробити так:

public class NotifySystem
{
private IMessageDelivery _messageDelivery;

public public NotifySystem(IMessageDelivery messageDelivery)
{
_messageDelivery = messageDelivery;
}

public void NotifyOnWarning(string message)
{
_messageDelivery.SendMessage(message);
}
}

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

— А можна було б просто не морочитися усіма цими принципами і писати код як вийде?
— Звичайно!
— І все одно все б так само працювало?
— Звичайно!
— Для чого ж тоді потрібно використовувати всі ці складності у вигляді ООП, ООД? Якщо я не буду думати про всіх цих складнощах — я набагато швидше напишу програму! А чим швидше напишу, тим краще!
— Якщо програма досить проста, а її подальший розвиток, доопрацювання або виправлення помилок не передбачається, то все це, в принципі, і не треба. Але! Якщо програма складна, планується її доопрацювання та підтримка, то застосування всіх цих «зайвих складнощів» буде безпосередньо впливати на найголовніше – на кількість витрачених ресурсів, необхідних для доопрацювань і підтримки, а це означає – на вартість.


Всі ці правила, шаблони, парадигми об'єднує одна основна думка – зробити код модульним, гнучким, легко розширюваним і стійким до змін. Зміна поведінки таких маленьких, слабосвязанних модулів, як правило, коштує дешевше, ніж зміни поведінки великого модуля, в якому все змішано до купи.

Предметна область
— Запам'ятай, якщо поганий алгоритм буде коштувати тобі тисячу, то погана архітектура буде коштувати тобі мільйон.

Тим не менш, все, що було описане вище – це, в основному, рішення технічних проблем. Про те, що таке ООП, ООД, які плюси у них є і як їх використовувати розписано вже десятки років тому. Досить вбити пару ключових слів, наприклад, SOLID і отримати купу пояснювальної інформації.

Якщо проблема досить загальна і вимагає для свого вирішення якої-небудь алгоритм, то, швидше за все, він теж вже є — досить лише простягнути руку. Кілька запитів в пошуковій системі, наприклад, «порахувати вагони, зчеплені по колу, рішення» і сотні результатів перед вами.

Але для того, щоб правильно розділити логіку на модулі і сервіси — одних технічних знань недостатньо, і пошуковик тут вже ніяк не допоможе. Потрібно гарне розуміння предметної області, а з цим вже проблеми, так як треба прикладати багато зусиль для вивчення чого-то, зовсім з програмуванням не пов'язаного. Тому розробники більше люблять вирішувати технічні проблеми, віддаючи вирішення бізнес-проблем на відкуп аналітиків. Але аналітикам невідомо внутрішній устрій програми, яке відображає не структуру бізнесу, а уявлення розробника про цю структуру. І чим далі це подання від реальності, тим складніше цю структуру буде змінювати для відповідності змінам, які відбулися в бізнесі.

Це пов'язано в тому числі і з подвійною інтерпретацією задачі. Спочатку аналітик намагається зрозуміти те, що треба додати чи змінити у додатку, спілкуючись з бізнес-користувачами. Потім ці знання він намагається своїми словами передати розробнику. В результаті, істина може спотворюватися і всі ці спотворення можуть вилитися у не найкращу структуру програми.

Користувач висловив побажання про те, що в кімнати будинків днем повинен потрапляти денне світло так, щоб все було видно. Розробник, недовго думаючи, зробив у стіні круглу дірку і перевірив — виявилося, що світла все ще недостатньо. Тоді розробник зробив ще дві дірки поруч і знову перевірив. Переконавшись, що світла достатньо, розробник вирішив, що завдання виконане. За цим принципом діри були видовбані у всіх будинках. Вимоги були дотримані.
Але літо скінчилося і настала зима, а разом з нею і холоду. Злий користувач прибіг і почав лаятися на те, що в кімнатах стало страшенно холодно. При з'ясуванні причин стало ясно, що із-за дірок, видовбаних під сонячне світло, в кімнату потрапляє багато холодного повітря з вулиці і вона промерзає. Після з'ясування обставин виявилося, що користувач хотів звичайне вікно, але якісь вимоги забув згадати він, а щось по-своєму передав аналітик, і вийшло те, що вийшло. Так як всі промерзла – вирішувати проблему довелося на ходу, часу на розробку хороших вікон і переробку дірок під вікна в зимовий період вже не було. Тому було прийнято рішення обійтися «милицею» і закрити дірки поліетиленом. Такий спосіб допоміг позбавитися від сильного промерзання і дозволив залишити сонячне світло вдень. Але хто знає, скільки нових проблем неповне розуміння початкових вимог ще принесе...


Для того, щоб вирішити цю проблему між усіма учасниками розробки має виробитися спільне, однакове уявлення про те, що відбувається і що для чого потрібно. Для цього існує цілий підхід – Domain Driven Design, метою якого якраз і є вироблення загального розуміння і загальної термінології (спільної мови) серед всіх учасників розробки.

Погане розуміння моделі призводить до того, що розробник намагається займатися тим, що йому ближче і зрозуміліше – вирішенням технічних проблем. В тому числі і рішенням бізнес-проблем суто технічним способом. А в разі відсутності технічних проблем — їх винаходом і їх же героїчним рішенням. Все це буде призводити лише до появи дивних, малозрозумілих конструкцій в програмі. З іншого боку, це може бути усвідомленим вибором, якщо розробник слід Ipoteka Driven Development.

Але прийшла пора закінчувати невелику подорож за принципами ООД та інших абстрактних тем і повертатися до програми. Після додавання бізнес-логіки до нашої моделі, у нас вийшов вже не просто знімок застиглого міста, а його повноцінна модель. Модель, стан якої можна легко змінювати так само, як і стан реальної моделі.

Презентаційна логіка
Незважаючи на всі наші успіхи, користувачі продовжують нетерпляче чекати перед погаслої монітором. Ми створили модель нашого міста, вдихнули в нього життя, описавши логіку його зміни, виконавши тим самим дуже важливий обсяг роботи. Тепер нам залишилося дати користувачам можливість «побачити» те, що ми зробили. Для цього нам потрібно навчити наше додаток взаємодіяти з користувачами. По-перше, користувач хоче дивитися, що відбувається з його містом, як рухаються машинки, як загоряються нічні вікна, як кудись поспішають люди. А по-друге, він хоче впливати на місто — перемикати світлофор, змінювати день на ніч, створювати нові будинки і т. п.

Але охопити всю картинку цілком можливо тільки в простих випадках. Якщо наша модель складна, то відобразити її цілком на екрані, а вже тим більше зрозуміти, стає не просто складно, а практично неможливо. Найкращий спосіб боротьби зі складністю – це поділ на більш прості частини, як ми і поступимо. Ми будемо відображати стан нашої моделі по частинах. Найголовніше – це правильно визначити ці частини. Кожна така частина має бути по можливості самодостатньою. Самодостатньою для розуміння якоїсь корисної інформації або для прийняття якогось потрібного рішення про зміну. Отже, ми поділимо наше стан на частини у відповідності з додатковими завданнями і спробуємо відобразити їх користувачеві.

Наприклад, ми хочемо регулювати світлофор на перехресті. Для цього нам не потрібно знати які будинки стоять навколо, де працюють або живуть люди, які чекають на зелений сигнал світлофора або те, що відбувається на іншому кінці міста. Все, що нам потрібно для прийняття рішення — це інформація про перехресті і про те, що на ньому знаходиться. Скільки машин і на яких смугах коштує, скільки людей стоїть в очікуванні на пішохідному переході і який сигнал у світлофора. Перехрестя в розрізі цієї задачі — це кореневий об'єкт, з якою пов'язані інші об'єкти (машини, світлофор, люди). В разі іншого завдання, наприклад, управління всього транспортного потоку міста, наш перехрестя буде всього лише частиною іншого кореневого об'єкта – транспортної інфраструктури міста.

Відображаючи стан перехрестя на екрані користувача, по суті, ми показуємо його «фотографію» на момент звернення до програми, даючи користувачеві змогу вивчити її і прийняти рішення. Користувач може вивчити цю «фотографію» з усіх ракурсів і дізнатися для себе що-небудь корисне. Спираючись на ці знання, він може прийняти рішення про зміну стану моделі. Для цього він може, наприклад, переключити сигнал світлофора. Звичайно, наша фотографія може бути не застиглою, а анімованої. Періодично змінюватися та відображати зміни, що відбулися. Але все одно не буде жодних гарантій, що відображається саме останній стан.

В процесі перенесення реальної моделі у віртуальну – вона перетворилася в знеособлений набір параметрів і їх значень. І цей набір даних можна крутити і трансформувати, як тільки душі завгодно. Тому наш перехрестя, як і будь-яку іншу частину нашої моделі, можна відобразити величезним числом різних способів. І далеко не всі з них будуть зручні користувачеві. Це зручність буде виражатися у швидкості і якості прийняття рішення користувачем.

Наприклад, стан нашого перехрестя ми можемо відобразити на екрані у вигляді декількох списків. В одному ми перерахуємо всі машини, які на ньому зараз знаходяться, в іншому — людей, які стоять на переходах. Крім списків на екрані буде зображений набір перемикачів, відображають сигнал світлофора і кілька текстових полів з назвою і координатами перехрестя.


Або ж, ми може відобразити перехрестя в більш зручному для людини вигляді – намалювати його так само, як він виглядає в реальному моделі, розставити на ньому машинки і чоловічків, намалювати світлофор. Де-небудь у кутку відобразити міні-карту міста, значком позначивши розташування перехрестя. Очевидно, що в другому випадку сприйняття стану буде набагато більш зручним і швидким. Хоча в обох випадках на екрані відображаються одні і ті ж дані.


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

А якщо взяти що-небудь, типу Microsoft HoloLens, то взагалі можна відображати наш віртуальний місто так само, як він виглядав в реальності або навіть ще зручніше.

Збереження стану
Після декількох годин безперервної роботи користувач, з радістю від виконаної роботи, відкидається на спинку стільця.
— Уффф – вимовляє втомлений користувач.
— Чпок – відповідають йому пробки і світло в моніторі гасне.
В очах розробника з'являється вселенський смуток. Нічого не підозрюючи користувач вмикає світло назад і запускає комп'ютер. Запустивши свій віртуальний місто — користувач бачить, що все, що він встиг зробити – пропало. Місто виглядає точно так само, як і в момент першого запуску ...


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

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

Не будемо винаходити велосипед і вирішимо проблему збереження стану моделі за допомогою РСУБД. Крім зручності збереження і вилучення даних, вона надає додаткові корисні можливості, такі як, контроль цілісності даних, який реалізується за допомогою різних обмежень. Так само, база даних захищає від різних збоїв в момент збереження, не даючи можливості записати змінений стан лише частково, тим самим приводячи його в поламане стан.

В базі даних вся інформація зберігаються в таблицях. Як правило, для кожного типу об'єкта існує своя таблиця. А для кожного властивості об'єкта в цій таблиці існує колонка. Значить, для того, щоб зберегти примірник будь-якого типу — нам потрібно створити для нього відповідну таблицю, з відповідним набором колонок.

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

Ще одним цікавим способом збереження є зберігання не стану моделі, а для опису дій, які це стан поміняли. Це нагадує redo-лог в базах даних або запис шахової партії, у вигляді послідовності ходів. Такий підхід називається event sourcing.

— Слухай. Виходить, що створюючи структури для зберігання даних, додаючи до них обмеження, тригера, зв'язку між таблицями і т. п. ми дублюємо бізнес-обмеження, які вже є в моделі, на сервері додатка?
— Виходить, що так.
— Виходить, що ми вчимося тому, що дублювати – це погано, а тут рррраз і надублировали. Хммм....


Багатошаровість
Так як ми акуратно створювали нашу програму і намагалися не змішувати різні дії, то, в результаті, у нас виділилося кілька різних верств логіки.

Бізнес-логіка
Це та частина логіки програми, яка відповідає за зміну стану нашої моделі. Вона описує умови, які повинні дотримуватися для здійснення зміни і самі зміни. Їй відома і доступна тільки модель.

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

Логіка доступу до даних
Це та логіка програми, яка знає, як зберігати і завантажувати стан моделі. Їй відома модель і те, як її можна заповнити даними з бази даних.

Логіка програми
Це та логіка, яка зв'язує все воєдино, як клей. Їй відомо і про бізнес-логікою, і про логіку доступу до даних, і про презентаційної логіці. Вона об'єднує їх, допомагає їм і налагоджує один з одним взаємодія.

Дотримання такого поділу спрощує зміни програми. Чим менше верствам відомо одне про одного і чим більше їх логіки приховано один від одного, тим простіше їх міняти. Адже, по суті, яка різниця презентаційної логіці, де і як зберігається стан моделі? І навпаки, яка різниця логіці зберігання даних, де і як відображаються ці дані на інтерфейсі? Адже способів їх відображення може бути декілька і вони можуть дуже сильно відрізнятися один від одного. У той же час моделі, все одно, як додаток зберігає її стан або як цей стан відображається на екрані. Тільки зміна моделі або бізнес-логіки може зачепити інші шари, так як вони є центральною частиною програми.



2-tier
— Гей! – крикнув один користувач іншому. – Ти ж повинен був додати нову будівлю в цьому місці!
— Так я його і додав, ось, подивися. – відповів другий першого.
— Справді… – почухав потилицю перший. — Дивно, що я його не бачу у себе.
— Нічого дивного, – відповів їм розробник. – адже у кожного з вас своя база даних, тобто, по суті, у кожного свій власний місто.
— А навіщо нам кожному своє місто? – запитали роздратовані користувачі. – адже він у нас був один, загальний і ми хочемо один, спільний.
В очах розробника знову з'явилася вселенський смуток.


Для того, щоб бачити зміни один одного — користувачі повинні працювати з одним і тим же станом моделі. Так як стан нашої моделі зберігається в базі даних, це означає, що користувачі повинні використовувати загальну базу даних. Як правило, вона розташовується на окремому комп'ютері, до якого програми всіх користувачів мають доступ.

Тепер одночасно кілька користувачів можуть дивитися і змінювати один і той же стан моделі. Але з'явилася інша проблема – проблема конкурентного доступу. Її можна вирішити різними способами. Найбільш поширений спосіб – блокування ресурсів для одноосібного доступу перед внесенням в них змін, ніж ми і скористаємося. Для цього доведеться уважно вивчити всю програму і зрозуміти, в яких місцях нам потрібно додати блокування.

Добре, якщо завдання вимагає блокування малого числа ресурсів на короткий проміжок часу. Погано, коли доводиться блокувати велику кількість ресурсів і робити це тривалий час. У таких випадках можна спробувати застосуватиінший підхід.

Після фізичного поділу програми на два комп'ютера — у нас вийшла клієнт-серверна (двухзвенная) архітектура.

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

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

N-tier
— А мені б ще нормально з планшета керувати містом, а то там такі складні обчислення, що я засинаю раніше, ніж він вважатиме, поки я на роботу їжу з дому. – попросив один із користувачів.
— А мені потрібно, щоб деякі нові користувачі мали доступ до всіх дій. – сказав інший користувач.
В очах розробника з'явився вже знайомий погляд...


Для вирішення нових проблем доведеться перенести бізнес-логіку програми з локального комп'ютера на окремий, спеціально виділений для цього комп'ютер, який зазвичай називають сервером додатків.

Наше акуратне поділ логіки на шари зіграло нам на руку. Презентаційна логіка і бізнес-логіка і так були вже досить розлуки, так що тут залишилося тільки фізично розділити їх і створити додаткову логіку програми для їх взаємодії. Наш товстий клієнт, який містив в собі бізнес-логіку, значно «схуднув» і став тонким клієнтом.

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


2 як 3
Насправді, для вирішення проблем, перерахованих вище, не обов'язково мати триланкову архітектуру. Вирішити ці проблеми можна і двухзвенной архітектурі, перемістивши бізнес-логіку в базу даних, тим самим залишивши клієнт тонким. У такого підходу є як свої плюси, так і свої мінуси.

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

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


Сервіси
— У нас тут вчора зламалося управління транспортом в місті й з-за цього не працювала вся програма. – почав один з користувачів, – Але, з управлінням транспорту ніяк не пов'язане управління електроенергією або водопровідною системою – продовжив він. – Можна якось зробити так, щоб можливість їх використання не залежала від проблем пов'язаних з ними областях? – запитав користувач в підсумку.
— У мене теж є прохання – продовжив інший користувач – Можна зробити так, щоб для оновлення різних можливостей програми не потрібно було б чекати досить довго оновлення всієї програми? – запитав він.


Витративши досить часу на самому початку і добре зрозумівши предметну область, ми змогли виділити групу сервісів. Наприклад, сервіс управління світлофорами, сервіс управління електроенергією або навіть сервіс управління будівлями. Всі ці сервіси є частиною нашого віртуального міста, але між собою вони пов'язані досить мало, а часто, не пов'язані зовсім. Тому код програми був написаний так само, з розбивкою на сервіси. Щоб цього досягти — ми виділили необхідний для кожного сервісу публічний набір методів, сховавши все інше. Таким чином, ми перетворили наш сервіс в «чорний ящик». Цього легко вдалося досягти, використовуючи різні засоби мови, такі як: модифікатори доступу методів, інтерфейси, абстрактні класи і т. п.


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

Щоб впоратися з цією проблемою, ми фізично розділили програму на кілька частин. Таким фізичним поділом може бути виділення сервісів у різні файли (бібліотеки). Це дозволило розбити програму на декілька файлів – основний запускається файл і набір бібліотек.

З допомогою такого фізичного поділу з'являється можливість незалежного оновлення сервісів, що представляє просту заміну сервісу бібліотеки. Незважаючи на те, що сервіси стали фізично розділені, вони продовжують використовувати загальну логіку програми, що досить зручно, та живуть в одному процесі, що дозволяє використовувати всі можливості єдиного додатка. Крім цього, таке явне розділення дозволяє деяким розробникам працювати тільки над певним сервісами.



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

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

До плюсів такого підходу можна віднести можливість використання для кожного окремого сервісу свого набору технологій. Що, безумовно, є чудовою приманкою для розробників. Але, в такому разі пропаде взаємозамінність розробників, що може стати недоліком.

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


— Слухай, а що всі так носяться з цими микросервисами?
— Розробники думають, що фізично розділивши бізнес-логіку, зможуть писати слабосвязанних, легкозамінні стійкі модулі.
— Як ніби їм раніше щось заважало…


Будь-яке фізичне розділення вирішує одні технічні проблеми, але натомість привносить проблеми інші. Вирішувати початкові технічні проблем, безумовно, треба, але завжди варто подумати, чи перевищить вигода від їх рішення недолік від появи нових проблем.

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

Інструменти
-А навіщо, замість того, щоб зробити нормальний запит до БД, він витягує обидві таблиці на сервер програми, потім героїчно об'єднує їх і фільтрує результат для отримання необхідних даних?
— Коли у тебе в руках тільки молоток, всі завдання здаються цвяхами.


Мови, бібліотеки, фреймворки, утиліти – це все те, чим користується розробник у процесі роботи. Все це потрібно для максимального зниження вартості рішення технічних проблем, залишаючи більше часу для вирішення унікальних для проекту проблем бізнесу. Тому правильний вибір інструментів важливий, оскільки безпосередньо впливає на продуктивність праці. Грубо кажучи, можна щось півдня випилювати пилкою, а можна взяти електролобзик — вжик і готово!

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

Але, серед розробників існує один дуже поганий синдром під назвою – «Not invented here». Його сенс в тому, що замість того, щоб взяти який-небудь готовий інструмент для вирішення певного кола завдань, розробники починають винаходити свій. Найбільш сильно це може проявлятися в бажанні створення своєї платформи.

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

Теорія та практика
В теорії, теорія і практика одне і теж. На практиці — ні.

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

В реальності розробники часто стоять перед компромісом між якістю і швидкістю. Спроба догодити замовнику і вкластися в термін змушує робити все похапцем. Це, в підсумку, дає зворотний ефект. Спішне рішення проблем змушує створювати погані рішення, які, в підсумку, накопичуються як великий сніжний ком, намагаючись поховати під собою весь проект. З-за них підтримка і виправлення починають коштувати ще дорожче, що тільки посилює проблему, особливо в поясненнях замовнику, чому вартість однотипних змін стала коштувати дорожче.

Такі погані рішення практично неминучі в будь-якому проекті, тому дуже важливо знаходити час на рефакторинг.

Підсумок
В кінцевому підсумку, все впирається в гроші. Чим дешевше вартість розробки і підтримки, тим вигідніше створювати програму. Тому всі підходи/інструменти/шаблони/парадигми і т. п. націлені на одне – здешевлення процесу розробки і підтримки.

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

І тільки від розробника залежить наскільки хорошим буде цей шлях. Тому в розробці програм люди — це головний і найважливіший ресурс.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.