# Template metóda

Častokrát sa nám stane, že potrebujeme vytvoriť 2 veľmi podobné procesy alebo algoritmy, pričom sa líšia naozaj minimálne. Niekedy stačí použiť príznak (jednoduchá boolovská premenná), avšak pri komplexnejších scenároch sa to veľmi rýchlo stáva neefektívne. Preto potrebujeme robustnejšie riešenia. Jedných z nich je vzor šablónová metóda (angl. template method), ktorej princíp si ukážeme na modelovej situácii.

Poznámka

Abstraktná factory metóda je špeciálny prípad template metódy.

# Modelová situácia

Odladiť malé aplikácie ide pomerne jednoducho, no akonáhle pracujeme s komplexnejšími a rozsahovo veľkými systémami, musíme zvoliť lepšie prostriedky monitorovania. Absolútnym základom je logovanie správ. Vďaka ním vieme dostať zo systému cenné informácie, ako sa choval počas toho, keď sa napr. objavila nejaká chyba a my sme sa vtedy "nepozerali". Všeobecne vieme rozdeliť logy na 2 typy - na vylaďovacie a chybové. Vylaďovacie sa zvyknú používať len pri vývoji a na produkčných strojoch skrývať, kým chybové chceme vypisovať vždy.

Aj keď je táto problematika už dávno vyriešená, dostali sme za úlohu vytvoriť náš vlastný logovací systém, ktorý musí spĺňať nasledovné podmienky:

  • Musí podporovať vypnutie a zapnutie zapisovania vylaďovacích správ
  • Musí podporovať 2 módy - výpis do konzole a výpis do súboru
  • Musí byť do budúcna pripravený na rozšírenie - napr. na mód, kedy sa správy odosielajú na vzdialený server

Skúsime začať naivne a rovno vytvorme triedu pre logovanie do konzoly. Mohla by vyzerať nasledovne:

public class Logger {

	private boolean showDebugMessages = true;

	public void debug(String message) {
		print(true, message);
	}

	public void error(String message) {
		print(false, message);
	}

	public void setShowDebugMessages(boolean showDebugMessages) {
		this.showDebugMessages = showDebugMessages;
	}

	public boolean shouldShowDebugMessages() {
		return showDebugMessages;
	}

	private void print(boolean isDebug, String message) {
		// Do not print debug messages if option is disabled
		if (isDebug && !showDebugMessages) {
			return;
		}

		String formattedMessage = String.format("[%s][%s] %s",
			LocalDateTime.now(),
			isDebug ? "DEBUG" : "ERROR",
			message
		);

		System.out.println(formattedMessage);
	}
}

Je toho trošku viac, poďme si všetko postupne rozobrať.

Aby sme dokázali na produkčných serveroch vypnúť logovanie vylaďovacích správ (ktorých je častokrát veľa), potrebujeme jednoduchý prepínač:

private boolean showDebugMessages = true;

public void setShowDebugMessages(boolean showDebugMessages) {
	this.showDebugMessages = showDebugMessages;
}

public boolean shouldShowDebugMessages() {
	return showDebugMessages;
}

Teraz si musíme pripraviť metódy pre vypísanie vylaďovacích a chybových správ. Nakoľko ich funkcionalita bude až na malý rozdiel tá istá, budeme z oboch volať len privátnu metódu print, ktorá sa postará o zvyšok:

public void debug(String message) {
	print(true, message);
}

public void error(String message) {
	print(false, message);
}

V samotnej print metóde najprv overíme, či nejde o situáciu, kedy sa má vypisovať vylaďovacia správa, avšak tento mód je vypnutý. Následne si správu trošku naformátujeme - pridáme aktuálnu časovú známku a taktiež kategóriu správy. Ako posledný krok je výpis - zatiaľ len do konzoly.

private void print(boolean isDebug, String message) {
	// Do not print debug messages if option is disabled
	if (isDebug && !showDebugMessages) {
		return;
	}

	String formattedMessage = String.format("[%s][%s] %s",
		LocalDateTime.now(),
		isDebug ? "DEBUG" : "ERROR",
		message
	);

	System.out.println(formattedMessage);
}

Zatiaľ je všetko fajn, no čoskoro sa to zmení - musíme pridať podporu módu pre zápis logov do súboru. Pokračujúc s našou naivnou verziou, skúsime urobiť niekoľko úprav, aby sme pri konštruovaní triedy Logger vedeli povedať, ktorý mód chceme použiť:

public class Logger {

	private static final String FILENAME = "application.log";

	private final boolean consoleOutput;
	private final PrintWriter printWriter;

	public Logger(boolean consoleOutput) {
		consoleOutput = consoleOutput;

		if (!consoleOutput) {
			FileWriter fileWriter = new FileWriter(FILENAME, true);
			BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
			printWriter = new PrintWriter(bufferedWriter);
		} else {
			fileWriter = null;
			bufferedWriter = null;
			printWriter = null;
		}
	}

	private void print(boolean isDebug, String message) {
		// Do not print debug messages if option is disabled
		if (isDebug && !showDebugMessages) {
			return;
		}

		String formattedMessage = String.format("[%s][%s] %s",
			LocalDateTime.now(),
			isDebug ? "DEBUG" : "ERROR",
			message
		);

		if (consoleOutput) {
			System.out.println(formattedMessage);
		} else {
			printWriter.println(formattedMessage);
		}
	}

	// rest is unchanged and omitted for brevity
}

Ako vidíte, takýmto prístupom sme vytvorili len zbytočnú komplexitu v kóde, urobili sme ho menej udržiavateľným a už vôbec nie modulárnym. Preto je na rade refaktorizácia, pričom budeme myslieť na to, že chceme použiť vzor template metódu.

Ako prvý krok si vytvoríme rozhranie, ktoré nám bude definovať funkcionalitu každého loggera. To bude pozostávať z metód na logovanie a povolenie/zakázanie ladiaceho módu:

public interface Logger {

	void debug(String message);

	void error(String message);

	void setShowDebugMessages(boolean showDebugMessages);

	boolean shouldShowDebugMessages();
}

Následne vytvoríme triedu AbstractLogger, ktorá bude implementovať Logger rozhranie. Ten nám bude definovať všeobecné správanie a spracovanie správ, pričom jediné, čo nedefinujeme, bude ako správu zapíšeme:





































 


 


public abstract class AbstractLogger implements Logger {

	private boolean showDebugMessages = true;

	@Override
	public void debug(String message) {
		print(true, message);
	}

	@Override
	public void error(String message) {
		print(false, message);
	}

	@Override
	public void setShowDebugMessages(boolean showDebugMessages) {
		this.showDebugMessages = showDebugMessages;
	}

	@Override
	public boolean shouldShowDebugMessages() {
		return showDebugMessages;
	}

	private void print(boolean isDebug, String message) {
		// Do not print debug messages if option is disabled
		if (isDebug && !showDebugMessages) {
			return;
		}

		String formattedMessage = String.format("[%s][%s] %s",
			LocalDateTime.now(),
			isDebug ? "DEBUG" : "ERROR",
			message
		);

		write(formattedMessage);
	}

	protected abstract void write(String message);
}

Všimnime si niekoľko vecí:

  • Implementácia AbstractLogger je skoro rovnaká, ako trieda Logger z nášho prvého pokusu
  • Rozhranie Logger sme plne implementovali - jeho správanie bude pre všetky podtriedy rovnaké (ak sa ho nerozhodnú preťažiť)
  • Zvýraznené riadky nám ukazujú template metódu - abstraktná časť v kostre metódy pre výpis, ktorá sa postará len o zápis, ale nijako inak neovplyvní predchádzajúce spracovanie

Aby sme sa funkcionálne vrátili tam, kde sme boli predtým, musíme implementovať ConsoleLogger, ktorý je teraz naozaj minimálny:

public class ConsoleLogger extends AbstractLogger {

	@Override
	protected void write(String message) {
		System.out.println(message);
	}
}

A čo zápis do súboru? Na to si vytvoríme trošku dlhšiu, no stále prehľadnú a nekomplexnú triedu FileLogger:

public class FileLogger extends AbstractLogger {

	private static final String FILENAME = "application.log";

	private final PrintWriter printWriter;

	public FileLogger() throws IOException {
		FileWriter fileWriter = new FileWriter(FILENAME, true);
		BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
		printWriter = new PrintWriter(bufferedWriter);
	}

	@Override
	protected void write(String message) {
		printWriter.println(message);
	}
}

Takéto rozloženie je perfektné pre modularitu, ktorá od nás bola požadovaná - ak by sme chceli vytvoriť logger, ktorý by odosielal správy na vzdialený server, potrebujeme len napísať kód, ktorý otvorí na tento server spojenie a postupne bude správy preposielať. O zvyšok však už máme postarané.

# Použitie v praxi

Aby sme videli silu toho, čo sme práve napísali, ukážme si, ako by sme logger v našom kóde použili. Predstavme si, že máme službu ArticleService, ktorá sa stará o nájdenie článku. Aby sme mali informácie o tom, čo sa deje, výsledok vyhľadávania vypíšeme ako vylaďovaciu správu a chybu pri získavaní z databázy ako chybovú:



 




 


 






public class ArticleService {

	private static final Logger logger = LoggerFactory.make();

	public ArticleEntity findArticle(int id) {
		try {
			Article article = database.findArticleById(id);
			logger.debug(String.format("Result for article id '%d': %s", id, article));
			return article;
		} catch (SQLException e) {
			logger.error("Unexpected database issue when retrieving article: " + e.getMessage());
		}
	}

	// assume that above code is correct and everything necessary is provided in the rest of this class
}

Vďaka tomu, ako sme Logger implementovali, zvyšok nášho kódu ho môže bez obáv používať, pričom sa nemusí starať o to, či, kde a ako bude správa zapísaná. Všetko zabezpečí naša pomyselná factory trieda LoggerFactory, ktorá sa už napr. na základe údajov z konfiguračného súbora rozhodne, ktorý z dostupných loggerov vráti.

# Best practices

  • Nebojte sa použiť viacero template metód v jednej triede
  • Ak máte pocit, že väčšina implementácií template metódy by bolo rovnakých a len niektoré by sa zmenili, neurobte ju abstraktou, ale implementujte predvolené správanie (napr. prázdne telo).

# Úlohy

Vzhľadom na jednoduchosť tohto návrhového vzoru neuvádzame žiadne ďalšie úlohy.

# Riešenia úloh

# Diskusia