Dizajn paterni intervju pitanja, 5 ključnih pitanja o dizajn paternima (3000 reči, 10 crteža), mora pročitati
Dizajn paterni su šabloni ili plaveprints koji se često koriste u softverskom inženjerstvu za rešavanje specifičnih problema, pomažu nam programerima da pišemo kod na jasniji, efikasniji i ponovo upotreljiviji način. Obično se dele u tri kategorije:
- Kreativni paterni: Uključuju instanciranje objekata, koriste se za kreiranje objekata, mogu povećati fleksibilnost i ponovnu upotrebljivost programa. Česti kreativni paterni su: metoda fabrike, apstraktna fabrika, singleton, graditelj, prototip itd.
- Strukturni paterni: Uključuju kombinaciju klasa i objekata, koriste se za dizajn strukture klasa i objekata kako bi se bolje realizovala funkcionalnost programa. Česti strukturni paterni su: adapter, most, kompozit, dekorater, fasada, fajvoter, proksi itd.
- Paterni ponašanja: Fokusiraju se na komunikaciju između objekata, uključuju lanac odgovornosti, komandu, interpreter, iterator, medijator, memo, posmatrača, stanje, strategiju, metodu šablona, posetioca itd.
01. Šta je lanac odgovornosti patern?
Preporučeno čitanje:
[refactoringguru.cn:lanac odgovornosti patern](https://refactoringguru.cn/design-patterns/chain-of-responsibility)
Patern lanca odgovornosti (Chain of Responsibility Pattern) je behavioralni dizajn patern, on daje više objekata priliku da procesiraju zahtev, čime izbjegava spregnutost između pošiljaoca i primaoca zahteva.
Zahtev se prenosi duž lanca dok neki objekat ne procesuje. Ovaj patern se često koristi za procesiranje različitih tipova zahteva i u situacijama kada se ne zna tačni primaoc da se zahtev prosledi jednom od više objekata.

Osnovni koncepti
Lanac odgovornosti patern uglavnom uključuje sledeće uloge:
- Handler (apstraktni procesor): Definiše interfejs ili apstraktnu klasu za procesiranje zahteva, obično sadrži referencu na sledećeg procesora u lancu.
- ConcreteHandler (konkretni procesor): Implementira metodu procesiranja apstraktnog procesora, ako može procesirati zahtev, procesira; inače prosleđuje zahtev sledećem procesoru u lancu.
- Client (klijent): Kreira lanac procesora i šalje zahtev prvom objektu procesora u lancu.
Proces rada
- Klijent šalje zahtev prvom objektu procesora u lancu.
- Procesor nakon prijema zahteva odlučuje da li ima sposobnost da procesira.
- Ako može procesirati, procesira zahtev.
- Ako ne može procesirati, prosleđuje zahtev sledećem procesoru u lancu.
- Proces se ponavlja dok neki procesor u lancu ne procesira zahtev ili dok u lancu nema više procesora.
Scenariji primene
Lanac odgovornosti patern je pogodan za sledeće scenarije:
- Postoji više objekata koji mogu procesirati isti zahtev, ali koji tačno objekat će procesirati odlučuje se u vreme izvršenja dinamički.
- Slanje zahteva jednom od više objekata bez eksplicitnog specificiranja primaoca.
- Kada je potrebna dinamička organizacija i upravljanje procesorima.
Prednosti i mane
Prednosti:
- Smanjuje spregnutost: Razdvaja pošiljaoca i primaoca zahteva.
- Povećava fleksibilnost dodele odgovornosti objektima: Može se dinamički menjati sastav lanca ili njihov redosled u vreme izvršenja.
- Omogućuje lako dodavanje novih procesorskih klasa, proširivanje funkcionalnosti bez uticaja na postojeći kod.
Mane:
- Zahtev možda neće biti procesiran: Ako nijedan procesor ne procesira zahtev, može doseći kraj lanca i biti odbačen.
- Problemi sa performansama: Zahtev može duže putovati kroz lanac, utiče na performanse.
- Teško debugiranje: Naročito kada je lanac duži, debugiranje može biti problematično.
Primer implementacije
Pretpostavimo da postoji sistem logovanja, prema nivou ozbiljnosti loga (greška, upozorenje, informacija) šalje poruke različitim procesorima.
abstract class Logger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;
protected int level;
// Sledeći element u lancu odgovornosti
protected Logger nextLogger;
public void setNextLogger(Logger nextLogger) {
this.nextLogger = nextLogger;
}
public void logMessage(int level, String message) {
if (this.level <= level) {
write(message);
}
if (nextLogger != null) {
nextLogger.logMessage(level, message);
}
}
abstract protected void write(String message);
}
class ConsoleLogger extends Logger {
public ConsoleLogger(int level) {
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Standardna konzola::Logger: " + message);
}
}
class ErrorLogger extends Logger {
public ErrorLogger(int level) {
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Greška konzola::Logger: " + message);
}
}
class FileLogger extends Logger {
public FileLogger(int level) {
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("Fajl::Logger: " + message);
}
}
public class ChainPatternDemo {
private static Logger getChainOfLoggers() {
Logger errorLogger = new ErrorLogger(Logger.ERROR);
Logger fileLogger = new FileLogger(Logger.DEBUG);
Logger consoleLogger = new ConsoleLogger(Logger.INFO);
errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);
return errorLogger;
}
public static void main(String[] args) {
Logger loggerChain = getChainOfLoggers();
loggerChain.logMessage(Logger.INFO, "INFO nivo");
loggerChain.logMessage(Logger.DEBUG, " Debug nivo");
loggerChain.logMessage(Logger.ERROR, "Error nivo");
}
}U ovom primeru, kreiran je lanac log procesora. Različiti nivoi logova će procesirati odgovarajući procesori. Lanac odgovornosti čini ekspanziju i održavanje log sistema fleksibilnijim.
Izlazni rezultat:
Standardna konzola::Logger: INFO nivo
Fajl::Logger: Debug nivo
Standardna konzola::Logger: Debug nivo
Greška konzola::Logger: Error nivo
Fajl::Logger: Error nivo
Standardna konzola::Logger: Error nivo02. Šta je fabrika patern?
Preporučeno čitanje:
[refactoringguru.cn:fabrika patern](https://refactoringguru.cn/design-patterns/factory-method)
Patern fabrike (Factory Pattern) pripada kreativnim dizajn paternima, uglavnom se koristi za kreiranje objekata, bez izlaganja logike kreiranja objekata klijentu.
U roditeljskoj klasi pruža metodu za kreiranje objekata, dozvoljava potklasi da odluči koji tip objekta instancirati.
Na primer, kamion Truck i brod Ship moraju implementirati interfejs transporta Transport, koji deklariše metodu deliver.
Kamioni implementiraju deliver metodu, ali deliver kamiona je transport na kopnu, dok deliver broda je transport na moru.

Kod koji poziva metodu fabrike (klijentski kod) ne mora da razlikuje razlike između različitih podklasa, samo poziva deliver metodu interfejsa.
Glavni tipovi fabrika paterna
①, Patern proste fabrike (Simple Factory): Uvodi koncept kreatora, odvaja kod instanciranja od poslovne logike aplikacije. Patern proste fabrike uključuje fabrika klasu, koja pruža metodu za kreiranje objekata.
class SimpleFactory {
public static Transport createTransport(String type) {
if ("truck".equalsIgnoreCase(type)) {
return new Truck();
} else if ("ship".equalsIgnoreCase(type)) {
return new Ship();
}
return null;
}
public static void main(String[] args) {
Transport truck = SimpleFactory.createTransport("truck");
truck.deliver();
Transport ship = SimpleFactory.createTransport("ship");
ship.deliver();
}
}②, Patern metode fabrike (Factory Method): Definiše interfejs za kreiranje objekata, ali potklasa odlučuje koju klasu instancirati. Metoda fabrike odlaže instanciranje klase na potklase.
interface Transport {
void deliver();
}
class Truck implements Transport {
@Override
public void deliver() {
System.out.println("Transport na kopnu");
}
}
class Ship implements Transport {
@Override
public void deliver() {
System.out.println("Transport na moru");
}
}
interface TransportFactory {
Transport createTransport();
}
class TruckFactory implements TransportFactory {
@Override
public Transport createTransport() {
return new Truck();
}
}
class ShipFactory implements TransportFactory {
@Override
public Transport createTransport() {
return new Ship();
}
}
public class FactoryMethodPatternDemo {
public static void main(String[] args) {
TransportFactory truckFactory = new TruckFactory();
Transport truck = truckFactory.createTransport();
truck.deliver();
TransportFactory shipFactory = new ShipFactory();
Transport ship = shipFactory.createTransport();
ship.deliver();
}
}Scenariji primene
- Komponente sloja pristupa podacima (DAL): Metoda fabrike je pogodna za sloj pristupa podacima, gde je potrebno kreirati različite konekcije sa bazom podataka na osnovu različitih baza (kao MySQL, PostgreSQL, Oracle). Metoda fabrike može sakriti ovu logiku instanciranja, pružiti samo jedinstveni interfejs za dobijanje konekcije sa bazom.
- Logovanje: Kada aplikacija treba da implementira više načina logovanja (kao logovanje u fajl, bazu ili udaljeni servis), može se koristiti fabrika patern za dizajn fleksibilnog log sistema, koji dinamički odlučuje koju metodu logovanja koristiti na osnovu konfiguracije ili okruženja.
Kako naneti unificirani prefiks imena niti kroz fabriku?
Može se kreirati nit kroz fabrika patern, i u metodi fabrike postaviti prefiks imena niti. Ovim se osigurava da sve niti kreirane kroz fabriku imaju unificirane standard imenovanja.
class NamedThreadFactory implements ThreadFactory {
private final String prefix;
private int count = 0;
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(prefix + "-" + count++);
return thread;
}
}
public class ThreadFactoryDemo {
public static void main(String[] args) {
ThreadFactory factory = new NamedThreadFactory("MyThread");
Runnable task = () -> {
System.out.println("Naziv niti: " + Thread.currentThread().getName());
};
for (int i = 0; i < 5; i++) {
Thread thread = factory.newThread(task);
thread.start();
}
}
}03. Šta je singleton patern?
Preporučeno čitanje:
[refactoringguru.cn:singleton patern](https://refactoringguru.cn/design-patterns/singleton)
Patern singleton (Singleton Pattern) je kreativni dizajn patern, osigurava da klasa ima samo jednu instancu, i pruža globalnu tačku pristupa za dobijanje te instance. Singleton patern se uglavnom koristi za kontrolu pristupa određenim deljenim resursima, na primer menadžer konfiguracije, pool konekcija, pool niti, log objekti itd.

Ključni momenti implementacije singleton paterna?
- Privatni konstruktor: Osigurava da spoljašnji kod ne može kreirati instancu klase kroz konstruktor.
- Privatna statička promenljiva instance: Drži jedinstvenu instancu klase.
- Javna statička metoda: Pruža globalnu tačku pristupa za dobijanje instance, ako instanca ne postoji, interno se kreira.
Uobičajene implementacije singleton paterna?
①. Kako implementirati singleton - eager način?
Eager singleton (Eager Initialization) kreira instancu u vreme učitavanja klase, bez obzira da li ćete kasnije koristiti ili ne, otuda i naziv eager, jednostavan je ali ne podržava odloženo učitavanje instance.
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}②. Kako implementirati singleton - lazy način?
Lazy singleton (Lazy Initialization) kreira instancu u vreme stvarnog korišćenja, "zaista lenj" (😂). Ovaj način implementacije treba da razmotri pitanje sigurnosti niti, stoga obično nosi [synchronized ključnu reč](https://javasi.dev/thread/synchronized-1.html).
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}③. Kako implementirati singleton - dvostruka provera?
Dvostruka provera koristi synchronized blok koda umesto synchronized metode. I dodaje [volatile ključnu reč](https://javasi.dev/thread/volatile.html) ispred instance, sprečava reordering instrukcija, jer instance = new Singleton() nije atomična operacija, može se reored-ovati, tako da druge niti mogu dobiti nekompletno inicijalizovanu instancu.
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}Kada se instance kreira, drugi poziv metode getInstance neće ući u synchronized blok, time se poboljšavaju performanse.
④. Kako implementirati singleton - statička unutrašnja klasa?
Koristi [statičku unutrašnju klasu](https://javasi.dev/oo/static.html) (Static Nested Class) i [mehanizam učitavanja klase](https://javasi.dev/jvm/class-load.html) Java-e za realizaciju thread-safe odložene inicijalizacije.
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}Prvi put kada se učita klasa Singleton neće inicijalizovati SingletonHolder, samo prvi put kada se pozove metoda getInstance će dovesti do učitavanja SingletonHolder, čime se instancira instance.
⑤. Kako implementirati singleton - enum?
Korišćenje [enum](https://javasi.dev/basic-extra-meal/enum.html) za implementaciju singletona je najjednostavniji način, ne samo da ne treba razmišljati o thread sinhronizaciji, već sprečava refleksione napade i probleme serializacije.
public enum Singleton {
INSTANCE;
// Može se dodati metoda instance
}Koje su prednosti singleton paterna?
Singleton patern osigurava da klasa ima samo jednu instancu, i pruža globalnu tačku pristupa ovoj instance.
Ovo je vrlo korisno za situacije gde je potrebno kontrolisati korišćenje resursa ili deliti resurse, na primer pool konekcija baze podataka, kroz singleton patern može se izbeći ponovljeno kreiranje i uništavanje resursa, time se poboljšava iskorišćenost resursa i performanse sistema.
Koliko načina implementacije singleton paterna postoji?
Postoji 5 načina implementacije singleton paterna, uobičajeni su: eager, lazy, dvostruka provera, statička unutrašnja klasa i enum.
Koja je razlika između dodavanja synchronized na blok koda i na metodu?
Kada se doda synchronized na metodu, zaključava se cela metoda, bilo koja nit prilikom poziva te metode dobija lock tog objekta, sve dok se metoda ne završi ne oslobađa lock.
Kada se doda synchronized na blok koda synchronized (Singleton.class), zaključava se Class objekat klase, svi synchronized blokovi te klase će se izvršavati serijski.
04. Koji dizajn paterni poznajete?
Singleton patern, strategija patern.
Kada je potrebno kontrolisati pristup resursima, poput menadžmenta konfiguracije, menadžmenta pool konekcija, često koristim singleton patern. On osigurava da postoji samo jedna globalna instanca i pruža globalnu tačku pristupa.
Kada postoji više algoritama ili strategija koje se mogu promeniti i koristiti, koristiću strategija patern. Kao Jishupai projekat, koristio sam strategija patern za integraciju višemodela kao Xunfei Xinghuo, OpenAI, Zhipu AI itd., realizovao sam servis inteligentnog asistenta koji može slobodno menjati bazu velikih modela.

Prednost strategija paterna je što ne treba pisati if/else odluke u kodu, nego različite AI servise enkapsulirati u različite strategija klase, kroz fabrika patern kreirati različite instance AI servisa, time se realizuje dinamička promena AI servisa.
Kasnije ako želite dodati novi AI servis, samo treba dodati novu strategiju klasu, nema potrebe menjati postojeći kod, time se poboljšava proširivost koda.
Želite napraviti funkciju eksporta podataka, mogući formati eksporta uključuju Excel, JSON, XML, ako koristite OOP metode, kako biste dizajnirali ovu implementaciju?
Koristiću strategija patern + fabrika patern za realizaciju.
Prvo, definišu interfejs DataExporter, koji sadrži metodu exporta export(data), za eksport podataka.
public interface DataExporter {
void export(List<Object> data, OutputStream outputStream) throws Exception;
String getContentType();
String getFileExtension();
}Zatim, za svaki format eksporta (Excel, JSON, XML) kreiram konkretne strategija klase, ove klase implementiraju ExportStrategy interfejs i pružaju odgovarajuću logiku eksporta.
public class ExcelExporter implements DataExporter {
@Override
public void export(List<Object> data, OutputStream outputStream) throws Exception {
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Data");
// Pisanje zaglavlja
if (!data.isEmpty()) {
Object firstRow = data.get(0);
Row headerRow = sheet.createRow(0);
Field[] fields = firstRow.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
headerRow.createCell(i).setCellValue(fields[i].getName());
}
// Pisanje podataka
for (int i = 0; i < data.size(); i++) {
Row dataRow = sheet.createRow(i + 1);
Object obj = data.get(i);
for (int j = 0; j < fields.length; j++) {
fields[j].setAccessible(true);
Object value = fields[j].get(obj);
dataRow.createCell(j).setCellValue(value != null ? value.toString() : "");
}
}
}
workbook.write(outputStream);
workbook.close();
}
@Override
public String getContentType() {
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
}
@Override
public String getFileExtension() {
return "xlsx";
}
}
public class JsonExporter implements DataExporter {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void export(List<Object> data, OutputStream outputStream) throws Exception {
objectMapper.writeValue(outputStream, data);
}
@Override
public String getContentType() {
return "application/json";
}
@Override
public String getFileExtension() {
return "json";
}
}
public class XmlExporter implements DataExporter {
@Override
public void export(List<Object> data, OutputStream outputStream) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.newDocument();
Element root = document.createElement("data");
document.appendChild(root);
for (Object obj : data) {
Element item = document.createElement("item");
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Element element = document.createElement(field.getName());
Object value = field.get(obj);
element.setTextContent(value != null ? value.toString() : "");
item.appendChild(element);
}
root.appendChild(item);
}
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
DOMSource source = new DOMSource(document);
StreamResult result = new StreamResult(outputStream);
transformer.transform(source, result);
}
@Override
public String getContentType() {
return "application/xml";
}
@Override
public String getFileExtension() {
return "xml";
}
}Zatim, kreiram fabrika klasu DataExporterFactory, za kreiranje odgovarajućih instanci strategija eksporta na osnovu formata eksporta.
public class DataExporterFactory {
private static final Map<String, DataExporter> exporters = new HashMap<>();
static {
exporters.put("excel", new ExcelExporter());
exporters.put("json", new JsonExporter());
exporters.put("xml", new XmlExporter());
}
public static DataExporter getExporter(String format) {
DataExporter exporter = exporters.get(format.toLowerCase());
if (exporter == null) {
throw new IllegalArgumentException("Nepodržani format eksporta: " + format);
}
return exporter;
}
public static Set<String> getSupportedFormats() {
return exporters.keySet();
}
}Konačno, definišem kontekstnu klasu servisa eksporta, na osnovu parametara zahteva biram odgovarajuću strategiju eksporta, i pozivam njenu metodu eksporta.
@Service
public class DataExportService {
public void exportData(List<Object> data, String format, OutputStream outputStream)
throws Exception {
DataExporter exporter = DataExporterFactory.getExporter(format);
exporter.export(data, outputStream);
}
public ResponseEntity<byte[]> exportDataAsResponse(List<Object> data, String format)
throws Exception {
DataExporter exporter = DataExporterFactory.getExporter(format);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
exporter.export(data, baos);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(exporter.getContentType()));
headers.setContentDispositionFormData("attachment",
"export_" + System.currentTimeMillis() + "." + exporter.getFileExtension());
return ResponseEntity.ok()
.headers(headers)
.body(baos.toByteArray());
}
}05. Šta je strategija patern?
Strategija patern je behavioralni dizajn patern, definiše niz algoritama, enkapsulira svaki algoritam, tako da oni mogu međusobno da se zamenjuju. Ovaj patern se obično koristi za realizaciju različitih poslovih pravila, gde svaka strategija enkapsulira specifično ponašanje ili algoritam.

Posebno pogodan za optimizaciju kompleksnih grananjih izjava (if-else) u programu.
U strategija paternu, postoje tri uloge: kontekst, interfejs strategije i konkretna strategija.
- Interfejs strategije: Definiše javni interfejs sve podržane algoritme.
- Konkretna strategija: Klasa koja implementira interfejs strategije, pruža konkretnu implementaciju algoritma.
- Kontekst: Klasa koja koristi strategiju. Obično sadrži referencu na interfejs strategije, može promeniti konkretnu strategiju u vreme izvršenja.

Na primer, u Jishupai-u, korisnici mogu slobodno menjati AI servise, na strani servera može se koristiti if/else odluka, ali ako kasnije treba dodati novi AI servis, treba menjati kod, to nije dovoljno fleksibilno.
Stoga, koristimo strategija patern, različite AI servise enkapsuliramo u različite strategija klase, kroz fabrika patern kreiramo različite instance AI servisa, time se realizuje dinamička promena AI servisa.
@Service
public class PaiAiDemoServiceImpl extends AbsChatService {
@Override
public AISourceEnum source() {
return AISourceEnum.PAI_AI;
}
}
@Slf4j
@Service
public class ChatGptAiServiceImpl extends AbsChatService {
@Override
public AISourceEnum source() {
return AISourceEnum.CHAT_GPT_3_5;
}
}
@Slf4j
@Service
public class XunFeiAiServiceImpl extends AbsChatService {
@Override
public AISourceEnum source() {
return AISourceEnum.XUN_FEI_AI;
}
}06. Šta je template patern?
Template patern (Template Method Pattern) je behavioralni dizajn patern, definiše skelet algoritma u jednoj operaciji, odlaže neke korake do potklasa za realizaciju. Template metoda omogućava potklasama da redefinišu određene specifične korake algoritma bez menjanja strukture algoritma.

U Spring framework-u, postoje mnoge primene template metoda. Na primer JdbcTemplate, RestTemplate itd., oni definišu opšti tok pristupa podacima, nam je samo potrebno pružiti specifični SQL ili callback metodu.

Kako se template patern konkretno implementira u Javi?
Template metoda se obično realizuje kroz apstraktnu klasu i nasleđivanje. Apstraktna klasa definiše template metodu, ta metoda sadrži redosled pozivanja niza koraka, dok se konkretni koraci realizuju u potklasi.
abstract class AbstractClass {
// Template metoda, definiše skelet algoritma
public final void templateMethod() {
stepOne();
stepTwo();
}
protected abstract void stepOne();
protected abstract void stepTwo();
}
class ConcreteClassA extends AbstractClass {
@Override
protected void stepOne() {
System.out.println("ConcreteClassA: Step One");
}
@Override
protected void stepTwo() {
System.out.println("ConcreteClassA: Step Two");
}
}
class ConcreteClassB extends AbstractClass {
@Override
protected void stepOne() {
System.out.println("ConcreteClassB: Step One");
}
@Override
protected void stepTwo() {
System.out.println("ConcreteClassB: Step Two");
}
}
public class TemplateMethodPatternDemo {
public static void main(String[] args) {
AbstractClass classA = new ConcreteClassA();
classA.templateMethod();
AbstractClass classB = new ConcreteClassB();
classB.templateMethod();
}
}Ništa me ne zadržava——osim cilja, čak i ako na obali ima ruža, hlada, mirne luke, ja sam brod bez veza.
Serijal sadržaja:
- Mianzheni Java SE 🡭
- Mianzheni Java kolekcijski framework 🡭
- Mianzheni Java konkurentno programiranje 🡭
- Mianzheni JVM 🡭
- Mianzheni Spring 🡭
- Mianzheni Redis 🡭
- Mianzheni MyBatis 🡭
- Mianzheni MySQL 🡭
- Mianzheni operativni sistemi 🡭
- Mianzheni računarske mreže 🡭
- Mianzheni RocketMQ 🡭
- Mianzheni distribuirani sistemi 🡭
- Mianzheni mikroservisi 🡭
- Mianzheni dizajn paterni 🡭
- Mianzheni Linux 🡭
- Mianzheni OpenClaw 🡭
- Mianzheni Vještine 🡭
