V cílovém adresáři spustit příkaz
git clone https://github.com/karelkrema/bsc-test.git
cd bsc-test
Pro snadný build a spouštění v unixovém prostředí je možné spustit
chmod u+x *.sh
Pak jsou k dispozici příkazy build.sh
, run.sh
a buildandrun.sh
.
Build volá mvn clean compile assembly:single
, což v /target vytvoří jar se všemi dependencemi a spouštěcí třídou v manifestu.
Run pak samozřejmě tento soubor spouští a předává mu jeden parametr z příkazové řádky (soubor s transakcemi, jeden je předpřipravený).
Buildandrun je pak kombinací obojího, takže spuštění buildandrun.sh transactions.txt
hned po vyklonování by mělo fungovat - tj. zkompilovat program a spustit ho s předpřipraveným souborem.
Program je tolerantní, neznámý, nebo prázdný příkaz vypíše nápovědu. Přijímá jeden argument - název souboru se sadou příkazů. Pokud soubor neexistuje, je argument ignorován.
Celá aplikace se skládá z několika komponent (ty jsou členěny po balíčcích) - a ve výsledku je z toho poměrně dost přeinženýrované řešení.
Komponenty tedy jsou následující:
- BalancesPerCurrency v balíčku "balances": Komponenta, která drží aktuální výši částky pro každou měnu. Umožňuje zvyšovat částky u jednotlivých měn, případně vytvořit snapshot aktuálního stavu, přičemž tyto dvě akce by neměly interferovat (řešeno jednoduše synchronizací nad instancí).
- Command v balíčku "commands" a "commands.impl". Má několik implementací a podstatou je propojení textového příkazu s určitou akcí. Obecně vzato bych tyto dvě věci držel spíš oddělené, ale takhle je to zase relativně jednoduché. Každá implementace commandu v metodě "matches" ověřuje, zda zadaný textový řetězec má spouštět daný command. Metoda execute pak obsahuje samotný kód, který se má vykonat. Celé to funguje tak, že při načtení řetězce (příkazu) se postupně prochází zaregistrované commandy, a pokud některý "matchuje", je vykonán. O toto se stará komponenta CommandMatcher.
- BalancesDumpAssembler v balíčku "dump" je zodpovědný za poskládání řetězce s výpisem aktuálních zůstatků.
- CurrencyTransactionReader v balíčku "io" přijímá input stream, prochází ho řádek po řádku a každý řádek zpracovává. Tentýž balíček obsahuje ještě Writer, což je jenom převlečené System.out.println(...).
- CurrencyTransactionParser z balíčku "parsing" je zodpovědný za parsování transakce z řetězce, tj. např "EUR 30".
- ScheduledDump z balíčku "scheduling" periodicky vypisuje dump.
- Balíček "model" pak obsahuje enum Currency a objekt CurrencyTransaction
Říkali jste, že jeden z kandidátů to měl napsané za dvě hodiny v jedné třídě na 300 řádek. Já zdaleka tak rychlý nebyl. Řádek bude cca stejně - ale těch tříd mám trochu víc :-)
Kdybych měl program rozvíjet dál, určitě bych prozkoumal a využil spring test knihovnu a napsal nějaké end to end integrační testy.
Takhle jsem testoval jednotlivé části, coverage je 61%.
Každopádně jsem se snažil aplikaci psát tak, aby psaní (za použití mocků) testů bylo co nejsnadnější.
Typické: Spring, JUnit, Guava.
Méně typické (aspoň pro mě, zkoušel jsem ji prvně): Project lombok pro eliminaci boilerplate kódu (https://projectlombok.org/features/index.html)
Viz. třída CurrencyTransaction, což je v podstaě immutabilní DTO objekt s gettery a toString(). S lombokem vypadá takto:
@Getter
@AllArgsConstructor
@ToString
public class CurrencyTransaction {
private final @NonNull Currency currency;
private final @NonNull BigDecimal amount;
}
Je zajímavé, že i na takovémto jednoduchém zadání se toho dá hodně ladit. Několik TODOs mi zbylo, například při neexistujícím vstupním souboru by se měla místo ignorace vypsat hláška.
Také chybí např. logování a bonusová část, která by ale v praxi byla poměrně jednoduchá - jedna komponenta s kurzy měn k dolaru a jeden command.
In a desired directory, run followint command:
git clone https://github.com/karelkrema/bsc-test.git
cd bsc-test
For an easy build&run on unix platform, you can run:
chmod u+x *.sh
This makes build.sh
, run.sh
a buildandrun.sh
commands runnable.
Build calls mvn clean compile assembly:single
, which creates a jar file with all necessarry dependencies and a main class in manifest in /target directory.
Run obviously runs this jar file and passes a single command line parameter to it (there is an example file ready to use).
Buildandrun is a combination of both so running buildandrun.sh transactions.txt
right after the cloning should work, ie. compile the program and run it with the sample file.
The application is tolerant so unknown (or empty) command prints help on the screen.
As mentioned above, it accepts one command line argument - a name of a file with predefined commands set. If the file does not exist, it is simply ignored.
A whole apllication is composed of several components (placed in several packages). As a result, the solution is a little bit overengineered.
So the components are following:
- BalancesPerCurrency in "balances" package: A component holding actual balance for each currency. It enables to increase a balance of particular currency, or create a snapshot of the current state. These two actions should not interfere, which is achieved by a simple synchronization on the component instance.
- Command in "commands" and "commands.impl" packages. There are several implementations and the purpose is a bond betwen textual command and a particular action. In general, it would be better to keep these two thing separated, but for sake of simplicity, i decided to couple them. Each implementation is able to check whether a given string matches the command and, eventually, execute the action of the command. The way it works is that when a command is read, a component called "CommandMatcher" iterates over registerd commands and the one who matches is executed. If no matching command is found, "FallbackCommand" is executed (it prints out a help message).
- BalancesDumpAssembler in "dump" package is responsible for an assembly of the string with actual balances dump.
- CurrencyTransactionReader in "io" io package accepts an input streams, walks through it line by line and processes each line. The same package also contains Writer component, which is just a wrapped System.out.println(...).
- CurrencyTransactionParser in "parsing" package is responsible for parsing a transaction out of a strings, ie. "EUR 30".
- ScheduledDump in "scheduling" package dumps the balances periodically.
- Package "model" then contains Currency enum and CurrencyTransaction objects.
You stated that one of the candidates had this exercise done in two hours, one class and 300 lines. I was definitely not that quick. The amount of lines is probably something similar, but the classel... I definitely have a little bit more of them :-)
If further development was necessary, I would definitely explore and use the spring test library and write down some end to end integration tests.
So far, I have tested particular units, the coverage is 61%.
The thing is I tried to write the app in a way that covering it with tests (and mocking) should be easy.
Typical ones: Spring, JUnit, Guava.
Less typical ones (at least for me, I have tried it for the first time): Project lombok for boilerplate code elimination (https://projectlombok.org/features/index.html).
See. CurrencyTransaction class, which is in fact just an immutable dto with getters and toString(). With lombok, it looks like this:
@Getter
@AllArgsConstructor
@ToString
public class CurrencyTransaction {
private final @NonNull Currency currency;
private final @NonNull BigDecimal amount;
}
It's interesting that even such a simple exercise could be tuned forever. I still have (quite) a few TODOs to complete, e.g. nonexistent input file should at least print some error message out.
Logging is missing as well, plus the bonus part (balances in USD - this would in fact be pretty simple to implement - one component to maintain the rates and one command).