Към съдържанието
wannacode

SOLID без догма

Накратко: SOLID са пет принципа за код, който понася промени: една причина за промяна, разширение без промяна, заменими подтипове, малки интерфейси и зависимост от абстракции.

Проблем

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

Онлайн магазинът праща на клиента фактура по имейл. Всяка вечер дава и на счетоводството файл с ред за всяка поръчка. Двете неща прави един клас:

record Order(String number, String customer, double net) {}
class InvoiceService {
public double vat(Order order) {
return order.net() * 0.20;
}
// Счетоводството: ред за файла, който внася в програмата си.
public String exportLine(Order order) {
double total = order.net() + vat(order);
return String.join(";", order.number(), money(order.net()), money(vat(order)), money(total));
}
// Клиентът: текстът на имейла с фактурата.
public String emailText(Order order) {
double total = order.net() + vat(order);
return String.format("Здравей, %s! Поръчка %s: %s €", order.customer(), order.number(), money(total));
}
private String money(double amount) {
return String.format("%.2f", amount);
}
}
InvoiceService invoices = new InvoiceService();
Order order = new Order("2026-0142", "Мария", 80);
System.out.println(invoices.exportLine(order));
System.out.println(invoices.emailText(order));
Изход
2026-0142;80.00;16.00;96.00
Здравей, Мария! Поръчка 2026-0142: 96.00 €

Маркетингът казва: „В имейла сумата да е 96 €, а не 96.00 €. Изглежда по-чисто.“ Някой сменя помощния метод money:

private String money(double amount) {
if (amount == Math.floor(amount)) {
return String.valueOf((long) amount);
}
return String.format("%.2f", amount);
}
Изход
2026-0142;80;16;96
Здравей, Мария! Поръчка 2026-0142: 96 €

Имейлът е какъвто го искаха. На сутринта счетоводната програма отказва файла: очаква 80.00, а получава 80. Редът за счетоводството никой не е пипал. Пипнат е помощният метод, който двата реда делят. Тестът за имейла минава, а тест за файла няма, защото „него никой не е сменял“.

Идеята

Една причина за промяна

Класът служи на двама, които не се познават. Счетоводителката иска точен формат, който нейната програма чете. Маркетингът иска приятен текст за клиента. Двамата искат промени по различно време и по различни причини. Щом кодът им е в един клас, промяната за единия може да счупи другия.

Това е първият от петте принципа SOLID (събрани от Робърт Мартин; името е от първите им букви на английски). Принципът за единствената отговорност (Single Responsibility Principle, SRP): класът има една причина за промяна.

„Причина“ значи кой иска промяната: човек, отдел, закон. Не значи „един метод“ и не значи „малък клас“. Въпросът, който си задаваш: „Кой ще дойде да поиска промяна тук?“ Ако отговорът е „счетоводството или маркетингът“, класовете са два.

Виж го

Всеки със своя клас

ДДС-то и сумата са правило на данъчния закон и трябват и на двамата. Те отиват при поръчката, на едно място:

record Order(String number, String customer, double net) {
public double vat() {
return net * 0.20;
}
public double total() {
return net + vat();
}
}

Редът за счетоводството и текстът на имейла стават отделни класове. Всеки форматира сумите по своя начин:

class AccountingExport {
public String line(Order order) {
return String.format("%s;%.2f;%.2f;%.2f", order.number(), order.net(), order.vat(), order.total());
}
}
class InvoiceEmail {
public String text(Order order) {
return String.format("Здравей, %s! Поръчка %s: %s €", order.customer(), order.number(), money(order.total()));
}
private String money(double amount) {
if (amount == Math.floor(amount)) {
return String.valueOf((long) amount);
}
return String.format("%.2f", amount);
}
}
Order order = new Order("2026-0142", "Мария", 80);
System.out.println(new AccountingExport().line(order));
System.out.println(new InvoiceEmail().text(order));
Изход
2026-0142;80.00;16.00;96.00
Здравей, Мария! Поръчка 2026-0142: 96 €

Имейлът е с цели суми, а файлът пак е с две цифри. Утре счетоводството ще смени формата, и ще пипаш само AccountingExport. Кодът на имейла дори не е пред теб.

Двата начина да напишеш сумата вече не са общи. Това не е дублиране: днес си приличат, но са две правила, които се сменят по различни причини.

Кога пречи. Другата крайност е клас за всяко действие: VatCalculator, TotalCalculator, NumberFormatter, GreetingBuilder, по три реда всеки. Сменят се винаги заедно, защото ги иска един и същ човек, а ти ги отваряш в четири файла, за да прочетеш едно правило. Клас от 200 реда, който служи на един човек, е по-добре от пет класа, които винаги се отварят заедно. Каквото се сменя заедно, стои заедно.

По-точно, за любопитните
  • Робърт Мартин сам пише, че „една отговорност“ често се чете като „прави едно нещо“. Неговото по-късно уточнение е, че модулът отговаря пред един актьор: група хора, които искат промени. „Прави едно нещо“ е правило за функциите, а не за класовете.
  • „Не се повтаряй“ (DRY) е за знание, не за текст. Два реда код, които изглеждат еднакво, но се сменят по различни причини, не са дублиране. Ако ги слееш в един помощен метод, връзваш двама души с един възел. Точно това стана в проблема.

Пробвай

Кой иска промяната

1. Кой въпрос ти казва дали класът има една причина за промяна?
2. ShippingLabel смята цената на доставката (иска я търговският отдел), рисува етикета (форматът е на куриерската фирма) и пише реда за склада (иска го складът). Как го подреждаш?

Виж го

Нова доставка, без да пипаш сметката

Вторият принцип е отворено/затворено (Open/Closed Principle, OCP): кодът е отворен за разширение и затворен за промяна. Нов вариант идва с нов код, а кодът, който вече работи и е проверен, не се пипа.

Видя го в урока Strategy: веригата от if за начина на плащане стана таблица име → стратегия. Доставките са същото. Всяка доставка знае цената си:

interface Delivery {
double cost(double amount);
}
class ToAddress implements Delivery {
@Override
public double cost(double amount) {
return amount >= 100 ? 0 : 5.90;
}
}
class ToOffice implements Delivery {
@Override
public double cost(double amount) {
return 3.90;
}
}

Сметката не знае какви доставки има:

static double orderTotal(double amount, Delivery delivery) {
return amount + delivery.cost(amount);
}

Магазинът добавя доставка до автомат. Тя е нов клас:

class ToLocker implements Delivery {
@Override
public double cost(double amount) {
return 2.90;
}
}

orderTotal, доставката до адрес и до офис не се пипат. Остава новата доставка да влезе в таблицата, от която клиентът избира. Това е следващата карта.

Кога пречи. Не можеш да затвориш кода за всичко, и не трябва. Всяка точка за разширение е интерфейс, клас и ред в таблица, които някой трябва да чете. Затваряш там, където промените наистина идват: нови доставки има всеки сезон. Правилото на опита: първия път пиши if. Когато дойде третият вариант, направи таблицата (Replace Conditional with Polymorphism от урока за refactoring).

Пробвай

Нов ред в таблицата

Таблицата от урока Strategy изброява доставките, от които клиентът избира. Добави автомата.

Какво пише на празното място?

Map<String, Delivery> deliveries = Map.of(
        "адрес", new ToAddress(),
        "офис", new ToOffice(),
        "автомат", );

for (String choice : List.of("адрес", "офис", "автомат")) {
    System.out.printf("%s: %.2f €%n", choice, orderTotal(80, deliveries.get(choice)));
}

Проблем

Отказът, който спря по средата

Клиентката плаща поръчка с три неща: 60 € с карта, ваучер за 20 € и 16 € от точки. После я отказва. Всеки вид плащане знае как да върне парите си:

interface Payment {
// Връща парите на клиента по същия начин, по който е платил.
void refund();
}
record CardPayment(double amount) implements Payment {
@Override
public void refund() {
System.out.printf("Карта: върнати %.2f €%n", amount);
}
}
record VoucherPayment(double amount) implements Payment {
@Override
public void refund() {
throw new UnsupportedOperationException("ваучер не се връща");
}
}
record PointsPayment(double amount) implements Payment {
@Override
public void refund() {
System.out.printf("Точки: върнати %.2f €%n", amount);
}
}

Поръчката минава по плащанията и вика refund() на всяко:

class Order {
private final List<Payment> payments;
String status = "платена";
Order(List<Payment> payments) {
this.payments = payments;
}
void cancel() {
for (Payment payment : payments) {
payment.refund();
}
status = "отказана";
}
}
Order order = new Order(List.of(
new CardPayment(60),
new VoucherPayment(20),
new PointsPayment(16)));
try {
order.cancel();
} catch (UnsupportedOperationException e) {
System.out.println(
"Грешка: " + e.getMessage());
}
System.out.println("Статус: " + order.status);
Изход
Карта: върнати 60.00 €
Грешка: ваучер не се връща
Статус: платена

Картата е върната, точките не са, а поръчката е още „платена“. Складът вижда „платена“ и изпраща пратката. Кодът на cancel() е правилен. Грешката е във ваучера: той е Payment, но не прави това, което Payment обещава.

Виж го

Подтипът, който не пази обещанието

void cancel() {
for (Payment payment : payments) {
payment.refund();
}
status = "отказана";
}
Order order = new Order(List.of(
new CardPayment(60),
new VoucherPayment(20),
new PointsPayment(16)));
try {
order.cancel();
} catch (UnsupportedOperationException e) {
System.out.println(
"Грешка: " + e.getMessage());
}
System.out.println("Статус: " + order.status);
Стъпка по стъпка, с думи
  1. Поръчката е платена с три неща: 60.00 € с карта, ваучер за 20.00 € и 16.00 € от точки. И трите са Payment.
  2. Клиентката отказва. cancel() минава по плащанията и вика refund() на всяко.
  3. Картата връща 60.00 €. Тя прави точно това, което Payment обещава.
  4. Ваучерът също е Payment, но неговият refund() хвърля UnsupportedOperationException. Обещанието е нарушено.
  5. Изключението излиза от цикъла и от cancel(). Точките (16.00 €) не са върнати, а редът, който прави status „отказана“, не се изпълнява.
  6. catch в main го хваща и печата грешката. Програмата продължава.
  7. Статусът е още „платена“. Складът ще изпрати пратката, а 60.00 € вече са върнати на картата.

Това е третият принцип, принципът на заместването на Барбара Лисков (Liskov Substitution Principle, LSP): където кодът работи с типа, може да застане всеки подтип, и кодът да не разбере. Подтипът може да върши работата по свой начин, но спазва обещанието на типа.

Payment обещава „връщам парите на клиента“. Какво значи това за ваучер в истински магазин? Нов ваучер за същата сума:

record VoucherPayment(double amount) implements Payment {
@Override
public void refund() {
System.out.printf("Ваучер: нов ваучер за %.2f €%n", amount);
}
}
Order order = new Order(List.of(
new CardPayment(60),
new VoucherPayment(20),
new PointsPayment(16)));
try {
order.cancel();
} catch (UnsupportedOperationException e) {
System.out.println(
"Грешка: " + e.getMessage());
}
System.out.println("Статус: " + order.status);
Изход
Карта: върнати 60.00 €
Ваучер: нов ваучер за 20.00 €
Точки: върнати 16.00 €
Статус: отказана

cancel() не е сменен. Подтипът вече пасва там, където се иска типът.

Нарушението се познава по три белега:

  • подтипът хвърля „не се поддържа“ (UnsupportedOperationException) за метод, който типът обещава;
  • подтипът връща нещо, което типът не обещава (-1, null, празен текст), или иска повече от извикващия;
  • кодът пита instanceof, преди да извика метода. Това е заобиколка около подтип, който не пасва.
Компилаторът не вижда нито едно от трите. Той проверява, че методът е написан, а не че прави обещаното.

Кога пречи. Тук догмата е обратна: да натикаш нещо в йерархията, защото „и то е плащане“. Ако нещо наистина не може да върне пари (например подарък от магазина), то не е Payment с refund. Дай му свой тип, вместо да пишеш метод, който хвърля.

По-точно, за любопитните
И стандартната библиотека има такъв подтип. List.of дава List, който не приема add:
List<String> items = List.of("кафе", "чаша");
try {
items.add("чай");
} catch (UnsupportedOperationException e) {
System.out.println("List.of не приема add: " + e.getClass().getSimpleName());
}
Изход
List.of не приема add: UnsupportedOperationException
Затова библиотеката го пише в документацията: методът е „по избор“ (optional operation). Ако твоят тип има метод, който понякога не работи, кажи го в името на типа или го раздели на два.

Формално, по Лисков: подтипът не иска повече от типа (не засилва условията на входа), не дава по-малко (не отслабва обещанието за резултата) и пази това, което за типа винаги е вярно.

Пробвай

Тихото нарушение

Доставката до автомат връща -1, когато автоматът е пълен. Сметката събира, както винаги. Какво печата програмата?

class ToLocker implements Delivery {
private final boolean full;
public ToLocker(boolean full) {
this.full = full;
}
@Override
public double cost(double amount) {
if (full) {
return -1; // -1 значи „няма място“
}
return 2.90;
}
}
System.out.printf("%.2f €%n", orderTotal(80, new ToLocker(true)));
Отговор

Проблем

SMS-ът, който никога не тръгна

Магазинът праща известия на клиентите. Някой е описал всичко, което може да се праща, на едно място:

interface Notifier {
void email(String to, String text);
void sms(String phone, String text);
}
Доставчикът на имейли праща само имейли, но интерфейсът иска и SMS. Без метода класът не се компилира, затова някой го е написал.

Пробвай

Намери грешката

Клиентката получава имейл, че пратката е тръгнала. SMS-ът за куриера не идва:

Notifier notifier = new MailProvider();
notifier.email("maria@example.com", "Поръчка 2026-0142 е изпратена.");
notifier.sms("0888 123 456", "Куриерът идва днес между 14 и 16 ч.");
Изход
Имейл до maria@example.com: Поръчка 2026-0142 е изпратена.

На кой ред е грешката?

Избери реда с грешката.

Виж го

Малки интерфейси

Четвъртият принцип: разделяне на интерфейсите (Interface Segregation Principle, ISP). Никой не трябва да зависи от методи, които не ползва. Договорът се пише от страната на този, който го ползва, и е толкова голям, колкото му трябва.

Имейлите и SMS-ите стават два интерфейса:

interface EmailSender {
void email(String to, String text);
}
interface SmsSender {
void sms(String phone, String text);
}

Всеки доставчик прави само каквото може, а известието за куриера иска само SMS:

class MailProvider implements EmailSender {
@Override
public void email(String to, String text) {
System.out.println("Имейл до " + to + ": " + text);
}
}
class SmsGateway implements SmsSender {
@Override
public void sms(String phone, String text) {
System.out.println("SMS до " + phone + ": " + text);
}
}
class ShippingNotice {
private final SmsSender sender;
public ShippingNotice(SmsSender sender) {
this.sender = sender;
}
public void courierToday(String phone) {
sender.sms(phone, "Куриерът идва днес между 14 и 16 ч.");
}
}
EmailSender email = new MailProvider();
email.email("maria@example.com", "Поръчка 2026-0142 е изпратена.");
new ShippingNotice(new SmsGateway()).courierToday("0888 123 456");
Изход
Имейл до maria@example.com: Поръчка 2026-0142 е изпратена.
SMS до 0888 123 456: Куриерът идва днес между 14 и 16 ч.

Ако някой подаде доставчика на имейли там, където трябва SMS, програмата не се компилира:

new ShippingNotice(new MailProvider()).courierToday("0888 123 456");
Грешка
error: incompatible types: MailProvider cannot be converted to SmsSender

Грешката от картата преди е невъзможна: празен метод няма кой да поиска.

Малките интерфейси помагат и на тестовете: двойникът за известието имплементира един метод, а не всичко, което магазинът може да праща. Един клас може да имплементира и двата, ако доставчикът наистина праща и имейли, и SMS: class MailAndSms implements EmailSender, SmsSender.

Кога пречи. Интерфейс за всеки метод „по ISP“ е другата крайност: Readable, Writable, Deletable, Countable пред хранилище, което се ползва на едно място и винаги цялото. Ако всеки, който ползва договора, ползва целия договор, той е един.

Проблем

Тестът, който праща истински имейл

Фактурата тръгва по имейл. Услугата сама си прави пощата:

class SmtpMailer {
public void send(String to, String text) {
System.out.println("SMTP до " + to + ": " + text);
}
}
class InvoiceService {
private final SmtpMailer mailer = new SmtpMailer();
public void issue(Order order, String email) {
mailer.send(email, "Фактура %s: %.2f €".formatted(
order.number(), order.total()));
}
}
Order order = new Order("2026-0142", "Мария", 80);
new InvoiceService().issue(order, "maria@example.com");
Изход
SMTP до maria@example.com: Фактура 2026-0142: 96.00 €

Работи, но:

  • тестът за фактурата праща истински имейл, а на машина без SMTP сървър пада;
  • магазинът минава от SMTP на услуга с API, и пипаш класа с правилата за фактурите, за да смениш пощата.

Правилото (какво пише във фактурата) зависи от детайла (как тръгва имейлът).

Виж го

Абстракцията е на правилото

Петият принцип: обръщане на зависимостите (Dependency Inversion Principle, DIP). Кодът с правилата не зависи от детайлите. И двете зависят от абстракция, и тя е на правилата: казва какво им трябва („прати този текст на този адрес“), не как го прави SMTP.

interface InvoiceSender {
void send(String to, String text);
}
class InvoiceService {
private final InvoiceSender sender;
public InvoiceService(InvoiceSender sender) {
this.sender = sender;
}
public void issue(Order order, String email) {
sender.send(email, "Фактура %s: %.2f €".formatted(
order.number(), order.total()));
}
}

Детайлите са отделно. Пощата и тестовият двойник имплементират абстракцията на услугата:

class SmtpSender implements InvoiceSender {
@Override
public void send(String to, String text) {
System.out.println("SMTP до " + to + ": " + text);
}
}
class FakeSender implements InvoiceSender {
final List<String> sent = new ArrayList<>();
@Override
public void send(String to, String text) {
sent.add(to + " | " + text);
}
}
Order order = new Order("2026-0142", "Мария", 80);
new InvoiceService(new SmtpSender())
.issue(order, "maria@example.com");
FakeSender fake = new FakeSender();
InvoiceService service = new InvoiceService(fake);
service.issue(order, "maria@example.com");
System.out.println("Тестът видя: " + fake.sent.get(0));
Изход
SMTP до maria@example.com: Фактура 2026-0142: 96.00 €
Тестът видя: maria@example.com | Фактура 2026-0142: 96.00 €

interface InvoiceSender {
void send(String to, String text);
}
class InvoiceService {
private final InvoiceSender sender;
public InvoiceService(InvoiceSender sender) {
this.sender = sender;
}
public void issue(Order order, String email) {
sender.send(email, "Фактура %s: %.2f €".formatted(
order.number(), order.total()));
}
}
class SmtpSender implements InvoiceSender {
@Override
public void send(String to, String text) {
System.out.println("SMTP до " + to + ": " + text);
}
}
Order order = new Order("2026-0142", "Мария", 80);
new InvoiceService(new SmtpSender())
.issue(order, "maria@example.com");
FakeSender fake = new FakeSender();
InvoiceService service = new InvoiceService(fake);
service.issue(order, "maria@example.com");
System.out.println("Тестът видя: " + fake.sent.get(0));
Стъпка по стъпка, с думи
  1. Преди фактурите сочеха надолу, към SmtpMailer. Сега услугата записва какво ѝ трябва: InvoiceSender, до себе си.
  2. InvoiceService получава пощата в конструктора и знае само InvoiceSender. За SMTP не знае нищо.
  3. SmtpSender имплементира InvoiceSender на услугата. Стрелката в кода сочи нагоре: детайлът зависи от правилото. Това е обръщането.
  4. При работа извикването пак върви надолу: от услугата до SMTP. Обърната е зависимостта в кода, не посоката на извикването.
  5. В теста същата услуга получава FakeSender: втори детайл, който сочи към същата абстракция. Имейл не тръгва: двойникът записва в sent какво е получил.
  6. Тестът вижда точно текста на фактурата, без пощенски сървър.

„Обръщането“ е на стрелката в кода. Преди фактурите знаеха за SMTP. Сега SMTP знае за InvoiceSender, който стои до фактурите. Извикването при работа пак върви от фактурите към SMTP.

DIP и DI. В урока Singleton и Dependency Injection видя подаването отвън: обектът получава зависимостта си в конструктора. Това е механизмът. DIP казва от какво да зависиш: от абстракция, която е твоя. Едното може без другото:

  • подаване без DIP: InvoiceService(SmtpMailer mailer). Пощата идва отвън, но услугата пак знае класа на SMTP, и тест без SMTP не става;
  • DIP без библиотека за DI: в main InvoiceService получава SmtpSender, и това е цялото сглобяване. Мястото, където програмата сглобява обектите, е едно.

Кога пречи. Интерфейс пред всеки клас „заради DIP“ е най-честата догма. Абстракция си заслужава пред детайл, който е бавен, външен или ще се сменя: поща, плащане, база, часовник, мрежа. Пред Order или пред форматирането на сума тя е код за четене без полза.

По-точно, за любопитните
В истински проект InvoiceSender е в пакета на фактурите (shop.invoices), а SmtpSender е в shop.mail и внася shop.invoices.InvoiceSender. Пакетът на фактурите не внася нищо от пощата. Това е обърнатата стрелка, видяна в import-ите.

Пробвай

Подреди теста

Тестът проверява фактурата без истинска поща. Поръчката order вече я има. Подреди редовете:

  1. InvoiceService service = new InvoiceService(fake);
  2. System.out.println("Тестът видя: " + fake.sent.get(0));
  3. service.issue(order, "maria@example.com");
  4. FakeSender fake = new FakeSender();

Накратко

Кога SOLID пречи

Ето ДДС-то, написано „по всички правила“:

interface TaxPolicy {
double vat(double net);
}
class StandardVat implements TaxPolicy {
@Override
public double vat(double net) {
return net * 0.20;
}
}
class TaxPolicyFactory {
public static TaxPolicy create() {
return new StandardVat();
}
}
System.out.printf("ДДС: %.2f €%n", TaxPolicyFactory.create().vat(80));
Изход
ДДС: 16.00 €

И същото без догма:

static final double VAT_RATE = 0.20;
System.out.printf("ДДС: %.2f €%n", 80 * VAT_RATE);
Изход
ДДС: 16.00 €

Изходът е един и същ. Първото е интерфейс, клас и фабрика за число, което се сменя веднъж на десет години, и то пак на едно място. Това е миризмата спекулативна общност: код за промени, които никой не е поискал. Когато дойде втора ставка (9% за храни), тогава е моментът за таблица.

Принципите са въпроси, не правила. Задавай ги, когато кодът боли:

Въпросът Догмата, която да избягваш
SRP Кой ще поиска промяна тук? Един или двама? Клас за всеки метод
OCP Кое наистина идва ново всеки месец? Точка за разширение навсякъде
LSP Може ли всеки подтип да застане тук без if? Йерархия заради думата „е“
ISP Ползва ли този клиент всичко, което иска? Интерфейс за всеки метод
DIP Бавен, външен или сменяем ли е детайлът? Интерфейс пред всеки клас

Упражнение

Подаръчна карта, без да пипаш отказа

Реши задачата в редактора си.

Подаръчна карта. Магазинът приема и подаръчни карти. При отказ парите се връщат обратно в картата. Добави GiftCardPayment, който спазва обещанието на Payment, и откажи поръчка, платена с 40 € от карта и 25 € от подаръчна карта. cancel() и другите плащания не се пипат.

Изход
Карта: върнати 40.00 €
Подаръчна карта: 25.00 € обратно в картата
Статус: отказана

Още задачи

  • Отчетът за месеца. Клас смята оборота за месеца, пише CSV за счетоводството и текст за управителя, и сам праща имейла. Раздели го по това кой иска промените. Кое остава общо?
  • Push известие. Добави известие в мобилното приложение, когато пратката е тръгнала. Кой малък договор ти трябва и кои класове не пипаш?
  • Махни една абстракция. Намери в свой проект интерфейс с една имплементация, зад която няма бавен или външен детайл. Махни обвивката и пусни тестовете.

Тест

Провери се

1. Маркетингът иска промяна в текста на имейла, а след нея пада файлът за счетоводството. Кой принцип е нарушен?
2. Магазинът добавя доставка до автомат. Какво значи „затворено за промяна“?
3. VoucherPayment имплементира Payment, но refund хвърля „не се поддържа“. Какво става с код, който вика refund на всяко плащане?
4. Доставчикът на имейли трябва да напише празен метод sms, за да имплементира Notifier. Какво ти казва това?
5. InvoiceService(SmtpMailer mailer) получава пощата в конструктора. Спазва ли DIP?
6. Кога интерфейс с една имплементация си заслужава?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·