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:
гарантира, че класът има само един екземпляр, и предоставя глобална точка за достъп до него.

Java
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;
}
}
Kotlin
object Singleton {
init {
println("Singleton instance has been created.")
}
fun doSomething() {
println("Singleton is doing something.")
}
}

Observer:
установява зависимост между обектите така, че когато един обект се промени, всички зависещи от него обекти получават уведомление за това и се обновяват автоматично.

Builder:
позволява да се създават обекти чрез поетапен процес, в който могат да се задават различни параметри, и накрая да се получи обект с определени свойства.

Kotlin
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)
}
}
val person = Person.Builder()
.firstName("John")
.lastName("Doe")
.build()

Factory:
предоставя общ интерфейс за създаване на обекти, но делегира същинското създаване на обектите на подкласовете.

Kotlin
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:
преобразува интерфейса на един клас в интерфейс, който очаква друг клас, за да могат да взаимодействат помежду си.

Kotlin
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 var mediaAdapter: MediaAdapter? = null
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:
динамично добавя нова функционалност към обектите, като ги обвива в други обекти, които имат тази функционалност.

Kotlin
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:
предоставя унифициран интерфейс към група интерфейси в една подсистема и така опростява взаимодействието с нея.

Kotlin
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:
определя основата на един алгоритъм, но позволява на подкласовете да променят някои от стъпките му, без да се променя общата му структура.

Kotlin
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:
При използването на шаблона „стратегия“ изнасяме част от логиката от основния код в отделни класове, които реализират общ интерфейс. След това в основния код можем да избираме нужната реализация в зависимост от определени условия, без да се налага да използваме множество условни оператори.

Kotlin
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)

Каква е разликата между наследяването и композицията

Наследяването и композицията са два различни подхода към проектирането на класове в обектно-ориентираното програмиране.

Наследяване:
това е механизъм, който позволява да се създаде нов клас на базата на вече съществуващ клас (базов клас или суперклас). При това новият клас (подклас или производен клас) наследява всички свойства и методи на базовия клас, а също така може да добавя свои собствени свойства и методи.

Kotlin
// Базов клас
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.
}

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

Kotlin
// Клас, представляващ двигател
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:
принцип за единствената отговорност. Класът трябва да има само една причина за промяна.

Лош вариант на реализация, тъй като в него има много причини за промяна:

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

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

Java
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:

Java
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:

Java
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:
принцип за заместване на Барбара Лисков. За да се създават софтуерни системи от взаимозаменяеми части, тези части трябва да отговарят на контракт, който позволява да се заменят една с друга. При наследяването не бива да засягаме функционалността на родителските класове

Лош вариант на реализация, в него се променя работата на методите на родителския клас:

Java
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();
// тук резултатът ще е 144, а не 120
}
}
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;
}
}

Правилен вариант на реализация:

Java
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:

Java
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");
}
}

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

Java
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. Те могат да опростят кода, да намалят дублирането му и да подобрят четимостта му.

Kotlin
// пример за функция от по-висок ред, която приема функция като аргумент
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