Roman Kryvolapov Engineering Blog

Java / Kotlin Core — Теоретичні питання

Які існують рівні складності у колекцій

Constant time (O(1)):
операції виконуються за постійний час, що не залежить від розміру колекції. Приклади операцій з такою оцінкою складності: додавання та видалення елементів з HashSet, отримання елемента за індексом з Array.

Logarithmic time (O(log n)):
операції виконуються за час, що логарифмічно залежить від розміру колекції. Приклади операцій з такою оцінкою складності: додавання та видалення елементів з TreeSet, пошук елемента в TreeMap.

Linear time (O(n)):
операції виконуються за час, що лінійно залежить від розміру колекції. Приклади операцій з такою оцінкою складності: пошук елемента в ArrayList, видалення елемента з LinkedList.

Linearithmic time (O(n log n)):
операції виконуються за час, лінійно помножений на логарифм від розміру колекції. Приклади операцій з такою оцінкою складності: сортування елементів у List з використанням алгоритму Merge Sort.

Quadratic time (O(n²)):
операції виконуються за час, що квадратично залежить від розміру колекції. Приклади операцій з такою оцінкою складності: сортування елементів у List з використанням алгоритму Bubble Sort.

Exponential time (O(2^n)):
операції виконуються за час, що експоненційно залежить від розміру колекції. Приклади операцій з такою оцінкою складності: обчислення всіх підмножин множини.

Які найчастіше використовувані патерни

Singleton:
гарантує, що у класу є лише один екземпляр, і надає до нього глобальну точку доступу.

public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
object Singleton {
init {
println("Singleton instance has been created.")
}
fun doSomething() {
println("Singleton is doing something.")
}
}

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

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

class Person private constructor(
val firstName: String?,
val lastName: String?,
) {
class Builder {
private var firstName: String? = null
private var lastName: String? = null
fun firstName(firstName: String) = apply { this.firstName = firstName }
fun lastName(lastName: String) = apply { this.lastName = lastName }
fun build() = Person(firstName, lastName, age, city, country)
}
}
val person = Person.Builder()
.firstName("John")
.lastName("Doe")
.build()

Factory:
надає спільний інтерфейс для створення об'єктів, але делегує фактичне створення об'єктів підкласам.

interface Transport {
fun deliver(): String
}
class Car : Transport {
override fun deliver() = "Delivering by car"
}
class Truck : Transport {
override fun deliver() = "Delivering by truck"
}
enum class TransportType {
CAR,
TRUCK,
}
object TransportFactory {
fun createTransport(transportType: TransportType): Transport {
return when (transportType) {
TransportType.CAR -> Car()
TransportType.TRUCK -> Truck()
}
}
}
val car = TransportFactory.createTransport(TransportType.CAR)
println(car.deliver()) // "Delivering by car"
val truck = TransportFactory.createTransport(TransportType.TRUCK)
println(truck.deliver()) // "Delivering by truck"

Adapter:
перетворює інтерфейс одного класу на інтерфейс, який очікує інший клас, щоб вони могли взаємодіяти один з одним.

interface MediaPlayer {
fun play(audioType: String, fileName: String)
}
interface AdvancedMediaPlayer {
fun playVlc(fileName: String)
fun playMp4(fileName: String)
}
class VlcPlayer : AdvancedMediaPlayer {
override fun playVlc(fileName: String) {
println("Playing vlc file. Name: $fileName")
}
override fun playMp4(fileName: String) {
// do nothing
}
}
class Mp4Player : AdvancedMediaPlayer {
override fun playVlc(fileName: String) {
// do nothing
}
override fun playMp4(fileName: String) {
println("Playing mp4 file. Name: $fileName")
}
}
class MediaAdapter(audioType: String) : MediaPlayer {
private val advancedMediaPlayer: AdvancedMediaPlayer?
init {
when (audioType) {
"vlc" -> advancedMediaPlayer = VlcPlayer()
"mp4" -> advancedMediaPlayer = Mp4Player()
else -> advancedMediaPlayer = null
}
}
override fun play(audioType: String, fileName: String) {
when (audioType) {
"vlc" -> advancedMediaPlayer?.playVlc(fileName)
"mp4" -> advancedMediaPlayer?.playMp4(fileName)
else -> println("Invalid media. $audioType format not supported")
}
}
}
class AudioPlayer : MediaPlayer {
private val mediaAdapter: MediaAdapter?
override fun play(audioType: String, fileName: String) {
when (audioType) {
"mp3" -> println("Playing mp3 file. Name: $fileName")
"vlc", "mp4" -> {
mediaAdapter = MediaAdapter(audioType)
mediaAdapter.play(audioType, fileName)
}
else -> println("Invalid media. $audioType format not supported")
}
}
}

Decorator:
динамічно додає об'єктам нову функціональність, обгортаючи їх в інші об'єкти, які мають цю функціональність.

abstract class Beverage {
abstract val description: String
abstract fun cost(): Double
}
abstract class CondimentDecorator : Beverage()
class Milk(private val beverage: Beverage) : CondimentDecorator() {
override val description = "${beverage.description}, Milk"
override fun cost() = beverage.cost() + 0.1
}
val espresso = Espresso()
val latte = Milk(Whip(Mocha(espresso)))

Facade:
надає уніфікований інтерфейс для групи інтерфейсів у підсистемі, спрощуючи таким чином взаємодію з нею.

class OrderProcessor {
fun processOrder(order: Order): String {
val warehouse = Warehouse()
val paymentSystem = PaymentSystem()
val warehouseResult = warehouse.checkInventory(order)
if (warehouseResult == "available") {
val paymentResult = paymentSystem.processPayment(order)
if (paymentResult == "success") {
warehouse.updateInventory(order)
return "Order processed successfully"
}
}
return "Order processing failed"
}
}
class Warehouse {
fun checkInventory(order: Order): String {
// check inventory
return "available"
}
fun updateInventory(order: Order) {
// update inventory
}
}
class PaymentSystem {
fun processPayment(order: Order): String {
// process payment
return "success"
}
}
class Order {
// order details
}
class OrderFacade {
fun processOrder(order: Order): String {
val warehouseResult = checkInventory(order)
if (warehouseResult == "available") {
val paymentResult = processPayment(order)
if (paymentResult == "success") {
updateInventory(order)
return "Order processed successfully"
}
}
return "Order processing failed"
}
private fun checkInventory(order: Order): String {
val warehouse = Warehouse()
return warehouse.checkInventory(order)
}
private fun updateInventory(order: Order) {
val warehouse = Warehouse()
warehouse.updateInventory(order)
}
private fun processPayment(order: Order): String {
val paymentSystem = PaymentSystem()
return paymentSystem.processPayment(order)
}
}

Template Method:
визначає основу алгоритму, але дозволяє підкласам перевизначити деякі кроки цього алгоритму без зміни його загальної структури.

abstract class Pizza {
fun make() {
prepareDough()
addIngredients()
bakePizza()
cutPizza()
}
protected fun prepareDough() {
println("Preparing pizza dough")
}
protected abstract fun addIngredients()
protected fun bakePizza() {
println("Baking pizza")
}
protected fun cutPizza() {
println("Cutting pizza")
}
}
class PepperoniPizza : Pizza() {
override fun addIngredients() {
println("Adding pepperoni to pizza")
}
}
class MargheritaPizza : Pizza() {
override fun addIngredients() {
println("Adding mozzarella and basil to pizza")
}
}
fun main() {
val pepperoniPizza = PepperoniPizza()
pepperoniPizza.make()
val margheritaPizza = MargheritaPizza()
margheritaPizza.make()
}
// Preparing pizza dough
// Adding pepperoni to pizza
// Baking pizza
// Cutting pizza
// Preparing pizza dough
// Adding mozzarella and basil to pizza
// Baking pizza
// Cutting pizza

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

interface PaymentStrategy {
fun pay(amount: Double)
}
class CreditCardStrategy(private val cardNumber: String, private val cvv: String) : PaymentStrategy {
override fun pay(amount: Double) {
// логіка оплати за допомогою кредитної картки
}
}
class PayPalStrategy(private val email: String, private val password: String) : PaymentStrategy {
override fun pay(amount: Double) {
// логіка оплати за допомогою PayPal
}
}
class PaymentProcessor(private val paymentStrategy: PaymentStrategy) {
fun processPayment(amount: Double) {
paymentStrategy.pay(amount)
}
}
// використання
val paymentStrategy = if (useCreditCard) {
CreditCardStrategy(cardNumber, cvv)
} else {
PayPalStrategy(email, password)
}
val paymentProcessor = PaymentProcessor(paymentStrategy)
paymentProcessor.processPayment(amount)

Які відмінності успадкування від композиції

Успадкування та композиція — це два різні підходи до проєктування класів в об'єктно-орієнтованому програмуванні.

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

// Базовий клас
open class Person(val name: String, val age: Int) {
open fun introduce() {
println("Hi, my name is $name and I am $age years old.")
}
}
// Клас-нащадок з додатковими параметрами в конструкторі
class Employee(name: String, age: Int, val jobTitle: String) : Person(name, age) {
override fun introduce() {
println("Hi, my name is $name, I am $age years old and I work as a $jobTitle.")
}
}
fun main() {
val employee = Employee("John", 30, "Software Developer")
employee.introduce() // Output: Hi, my name is John, I am 30 years old and I work as a Software Developer.
}

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

// Клас, що представляє двигун
class Engine(val type: String, val horsepower: Int) {
fun start() {
println("Engine of type $type with $horsepower HP started.")
}
}
// Клас, що використовує композицію
class Car(val model: String, val engine: Engine) {
fun drive() {
engine.start()
println("Driving a $model.")
}
}
fun main() {
val engine = Engine("V8", 450)
val car = Car("Mustang", engine)
car.drive() // Output: Engine of type V8 with 450 HP started.
// Driving a Mustang.
}

Відмінності між успадкуванням та композицією:

Зв'язок:
Успадкування — це відношення «є», де підклас є розширенням суперкласу. Композиція — це відношення «має», де об'єкт одного класу має посилання на об'єкт іншого класу.

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

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

Принцип підстановки Барбари Лісков:
Якщо успадкування використовується неправильно, воно може порушувати принцип підстановки Барбари Лісков (Liskov substitution principle), який стверджує, що будь-який екземпляр класу має бути замінним будь-яким екземпляром його підкласу без зміни коректності програми. У композиції цей принцип не порушується, оскільки об'єкти різних класів можуть бути замінені об'єктами, що реалізують один і той

Що таке SOLID

Принципи, зібрані Робертом Мартіном, автором Чистої архітектури, з неї і процитую відповіді

Single Responsibility Principle:
принцип єдиної відповідальності. У класу має бути лише одна причина для зміни.

Поганий варіант реалізації, оскільки в ньому багато причин для зміни:

class Man {
void work() {}
void eat() {}
void sleep() {}
}

Правильний варіант реалізації, у якому в кожному класі лише одна причина для зміни:

class Man {
}
class Work extends Man {
void work() {}
}
class Eat extends Man {
void eat() {}
}
class Sleep extends Man {
void sleep() {}
}
// патерн Фасад
class ManFacade {
Work work = new Work();
Eat eat = new Eat();
Sleep sleep = new Sleep();
void work() {
work.work();
}
void eat() {
eat.eat();
}
void sleep() {
sleep.sleep();
}
}

Open-Closed Principle:
принцип відкритості/закритості, програмні сутності мають бути відкриті для розширення та закриті для зміни

Поганий варіант реалізації, оскільки напрямок залежностей Main -> Toyota:

public class Main {
public static void main(String[] args) {
SportToyota sportToyota = new SportToyota();
workInTaxi(sportToyota);
}
static void workInTaxi(Toyota toyota){
if(toyota instanceof SportToyota){
((SportToyota) toyota).getOnePassenger();
} else {
toyota.getFourPassengers();
}
}
}
class Toyota {
void getFourPassengers(){ }
}
class SportToyota extends Toyota {
void getOnePassenger(){ }
}

Правильний варіант реалізації, оскільки напрямок залежностей Main -> Car <- Toyota:

public class Main {
public static void main(String[] args) {
SportToyota sportToyota = new SportToyota();
workInTaxi(sportToyota);
}
static void workInTaxi(Car car){
car.workInTaxi();
}
}
interface Car {
void workInTaxi();
}
class Toyota implements Car {
void getFourPassengers(){ }
@Override
public void workInTaxi() {
getFourPassengers();
}
}
class SportToyota extends Toyota {
void getOnePassenger(){ }
@Override
public void workInTaxi() {
getOnePassenger();
}
}

Liskov Substitution Principle:
принцип підстановки Барбари Лісков. Для створення програмних систем із взаємозамінних частин ці частини мають відповідати контракту, який дозволяє заміняти ці частини одна одною. При успадкуванні ми не повинні зачіпати функціонал батьківських класів

Поганий варіант реалізації, у ньому змінюється робота методів батьківського класу:

public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(10);
rectangle.setWeight(12);
rectangle.getSquare();
rectangle = new Square();
rectangle.setHeight(10);
rectangle.setWeight(12);
rectangle.getSquare();
// тут буде помилка
}
}
class Rectangle {
protected int weight;
protected int height;
void setWeight(int weight) {
this.weight = weight;
}
void setHeight(int height) {
this.height = height;
}
int getSquare() {
return weight * height;
}
}
class Square extends Rectangle {
@Override
void setWeight(int weight) {
this.weight = weight;
this.height = weight;
}
@Override
void setHeight(int height) {
this.height = height;
this.weight = height;
}
@Override
int getSquare() {
return weight * height;
}
}

Правильний варіант реалізації:

public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(10);
rectangle.setWeight(12);
rectangle.getSquare();
Square square = new Square();
square.setSide(10);
square.getSquare();
}
}
interface Shape{
int getSquare();
}
class Rectangle implements Shape {
protected int weight;
protected int height;
void setWeight(int weight) {
this.weight = weight;
}
void setHeight(int height) {
this.height = height;
}
@Override
public int getSquare() {
return weight * height;
}
}
class Square implements Shape {
int side;
public void setSide(int side) {
this.side = side;
}
@Override
public int getSquare() {
return side * side;
}
}

Interface Segregation Principle:
принцип розділення інтерфейсів. Уникати залежності від усього, що не використовується. Не повинно бути ситуацій, коли методи інтерфейсів не використовуються при їх реалізації.

Поганий варіант реалізації, у ньому метод work не використовується для Intern:

interface Worker {
void work();
void eat();
}
class Man implements Worker {
@Override
public void work() {
System.out.println("work");
}
@Override
public void eat() {
System.out.println("eat");
}
}
class Intern implements Worker {
@Override
public void work() {
// intern is study, not work
}
@Override
public void eat() {
System.out.println("eat");
}
}

Правильний варіант реалізації, розбиваємо інтерфейс на кілька:

interface Worker {
void work();
}
interface Eater {
void eat();
}
interface Person extends Worker, Eater {
}
class Man implements Person {
@Override
public void work() {
System.out.println("work");
}
@Override
public void eat() {
System.out.println("eat");
}
}
class Intern implements Eater {
@Override
public void eat() {
System.out.println("eat");
}
}

Dependency Inversion Principle:
принцип інверсії залежності. Код, що реалізує високорівневу політику, не повинен залежати від коду, що реалізує низькорівневі деталі. Навпаки, деталі мають залежати від політики. Усі залежності у вихідному коді перетинають цю межу в одному напрямку — у бік абстракції.
Абстрактний компонент містить усі високорівневі бізнес-правила застосунку. Конкретний компонент містить деталі реалізації цих правил.

Що таке функції вищого порядку

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

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

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

// приклад функції вищого порядку, яка приймає функцію як аргумент
fun applyOperation(
value: Int,
operation: (Int) -> Int
): Int {
return operation(value)
}
// приклад використання функції вищого порядку
val result = applyOperation(5) { value ->
value * 2
} // результат буде 10

У цьому прикладі функція applyOperation приймає аргумент operation, який є функцією, що приймає один аргумент типу Int і повертає результат типу Int. Усередині функції applyOperation, operation викликається з аргументом value, і результат повертається з функції applyOperation.

У чому відмінність структурної, функціональної та об'єктно-орієнтованої парадигми в програмуванні

Існують 3 парадигми в програмуванні, вони були відкриті між 1958 та 1968 роками. Кожна з парадигм накладає на код обмеження- парадигми говорять нам, що не можна робити.

Структурне програмування накладає обмеження на пряму передачу керування (наприклад за допомогою оператора goto / jump)

Об'єктно-орієнтоване програмування накладає обмеження на непряму передачу керування

Функціональне програмування накладає обмеження на присвоювання

див. Роберт Мартін, Чиста архітектура

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

JSON(JavaScript Object Notation):
був створений як альтернатива складнішим форматам обміну даними, таким як XML, і призначений для простоти читання та запису як людиною, так і комп'ютером. Формат JSON являє собою набір пар “ключ-значення”, де ключ — це рядок, а значення може бути будь-яким допустимим типом даних, включаючи об'єкти, масиви, числа, рядки, булеві значення та null.

SOAP (Simple Object Access Protocol):
ґрунтується на XML (Extensible Markup Language) і визначає формат повідомлення, а також правила його обробки.

Protobuf(Protocol Buffers):
бінарний формат, який не є людиночитним, на відміну від форматів, таких як XML і JSON. Натомість він являє собою послідовність байтів, які можна розібрати лише за допомогою спеціального інструмента. Сам протокол protobuf визначає мову опису даних, яка використовується для опису структури даних, що передаватимуться через мережу. Ця мова має свій синтаксис, який використовується для опису полів та їхніх типів даних. Кожне поле у структурі даних має унікальний ідентифікатор і тип даних. Дозволяє компактно та ефективно серіалізувати дані для передачі через мережу, при цьому забезпечуючи високу продуктивність і сумісність між різними версіями застосунків.

Copyright: Roman Kryvolapov