diff --git a/README.md b/README.md index 111b1ae..d801db1 100644 --- a/README.md +++ b/README.md @@ -22,101 +22,87 @@ 5. [References](#references) ## About - -This is a project of a library, driven by real [business requirements](#domain-description). -We use techniques strongly connected with Domain Driven Design, Behavior-Driven Development, -Event Storming, User Story Mapping. +This is a project of a library, driven by real [business requirements](#domain-description). We use techniques strongly connected with Domain Driven Design, Behavior-Driven Development, Event Storming and User Story Mapping. ## Domain description +- A public library allows **patrons** to place **books** on hold at its various **library branches**. +- Available books can be placed **on hold** only by one patron at any given **point in time**. + ++ Books are either **circulating or restricted**, and can have **retrieval or usage fees**. ++ A **restricted book** can only be held by a **researcher patron**. ++ A **regular patron** is limited to **five holds** at any given moment, while a **researcher patron** is allowed an **unlimited number +of holds**. + +- An **open-ended book hold** is **active** until the patron **checks out** the book, at which time it is **completed**. +- A **closed-ended book hold** that is **not completed** within a fixed number of days after it was requested will **expire**. This check is done at the beginning of a day by taking a look at **daily sheet** with **expiring holds**. + ++ Only a **researcher patron** can request an **open-ended hold duration**. ++ Any patron with more than two **overdue checkouts** at a library branch will get a **rejection** if trying a hold at that same library branch. ++ A book can be **checked out** for up to **60 days**. ++ Check for **overdue checkouts** is done by taking a look at **daily sheet** with overdue checkouts. + +- Patron **interacts** with his/her **current holds**, **checkouts**, etc. by taking a look at **patron profile**. +- **Patron profile** looks like a daily sheet, but the information there is limited to one patron and is not necessarily daily. +- Currently a patron can see **current holds** (not canceled nor expired) and **current checkouts** (including overdue). +- Also, he/she is able to **hold a book** and **cancel a hold**. + +How actually a patron knows which books are there to lend? The Library has its **catalogue of books** where books are added together with their **specific instances**. + +A **specific book instance** of a book can be added only if there is book with matching **ISBN** already in the catalogue. -A public library allows patrons to place books on hold at its various library branches. -Available books can be placed on hold only by one patron at any given point in time. -Books are either circulating or restricted, and can have retrieval or usage fees. -A restricted book can only be held by a researcher patron. A regular patron is limited -to five holds at any given moment, while a researcher patron is allowed an unlimited number -of holds. An open-ended book hold is active until the patron checks out the book, at which time it -is completed. A closed-ended book hold that is not completed within a fixed number of -days after it was requested will expire. This check is done at the beginning of a day by -taking a look at daily sheet with expiring holds. Only a researcher patron can request -an open-ended hold duration. Any patron with more than two overdue checkouts at a library -branch will get a rejection if trying a hold at that same library branch. A book can be -checked out for up to 60 days. Check for overdue checkouts is done by taking a look at -daily sheet with overdue checkouts. Patron interacts with his/her current holds, checkouts, etc. -by taking a look at patron profile. Patron profile looks like a daily sheet, but the -information there is limited to one patron and is not necessarily daily. Currently a -patron can see current holds (not canceled nor expired) and current checkouts (including overdue). -Also, he/she is able to hold a book and cancel a hold. - -How actually a patron knows which books are there to lend? Library has its catalogue of -books where books are added together with their specific instances. A specific book -instance of a book can be added only if there is book with matching ISBN already in -the catalogue. Book must have non-empty title and price. At the time of adding an instance -we decide whether it will be Circulating or Restricted. This enables -us to have book with same ISBN as circulated and restricted at the same time (for instance, -there is a book signed by the author that we want to keep as Restricted) +A Book must have non-empty **title** and **price**. At the time of adding an instance we decide whether it will be **Circulating** or **Restricted**. + +This enables us to have book with **same ISBN** as Circulated and Restricted at the same time (for instance, there is a book signed by the **author** that we want to keep as Restricted). ## General assumptions ### Process discovery +The first thing we started with was **domain exploration** with the help of **Big Picture EventStorming**. + +The **domain description** you found in the previous chapter, landed on our virtual wall: +![Event Storming Domain description](docs/images/eventstorming-domain-desc.png) -The first thing we started with was domain exploration with the help of Big Picture EventStorming. -The description you found in the previous chapter, landed on our virtual wall: -![Event Storming Domain description](docs/images/eventstorming-domain-desc.png) -The EventStorming session led us to numerous discoveries, modeled with the sticky notes: +The **EventStorming Session** led us to numerous **discoveries**, modeled with the sticky notes: ![Event Storming Big Picture](docs/images/eventstorming-big-picture.jpg) -During the session we discovered following definitions: + +During the session we discovered following **definitions**: ![Event Storming Definitions](docs/images/eventstorming-definitions.png) -This made us think of real life scenarios that might happen. We discovered them described with the help of -the **Example mapping**: +This made us think of **real life scenarios** that might happen. We discovered them described with the help of the **Example Mapping**: ![Example mapping](docs/images/example-mapping.png) -This in turn became the base for our *Design Level* sessions, where we analyzed each example: +This in turn became the base for our **Design Level Sessions**, where we analyzed each example: ![Example mapping](docs/images/eventstorming-design-level.jpg) -Please follow the links below to get more details on each of the mentioned steps: +Please follow the links below to get **more details** on each of the mentioned steps: - [Big Picture EventStorming](./docs/big-picture.md) - [Example Mapping](docs/example-mapping.md) - [Design Level EventStorming](docs/design-level.md) ### Project structure and architecture -At the very beginning, not to overcomplicate the project, we decided to assign each bounded context -to a separate package, which means that the system is a modular monolith. There are no obstacles, though, -to put contexts into maven modules or finally into microservices. +At the very beginning, not to over-complicate the project, we decided to assign each **bounded context** to a separate package, which means that the system is a **modular monolith**. There are no obstacles, though, to put contexts into **maven modules** or finally into **microservices**. -Bounded contexts should (amongst others) introduce autonomy in the sense of architecture. Thus, each module -encapsulating the context has its own local architecture aligned to problem complexity. -In the case of a context, where we identified true business logic (**lending**) we introduced a domain model -that is a simplified (for the purpose of the project) abstraction of the reality and utilized -hexagonal architecture. In the case of a context, that during Event Storming turned out to lack any complex -domain logic, we applied CRUD-like local architecture. +**Bounded Contexts** should (amongst others) introduce **autonomy** in the sense of architecture. Thus, each module encapsulating the context has its own local architecture aligned to problem complexity. + +In the case of a context, where we identified true business logic (**lending**) we introduced a **domain model** that is a simplified (for the purpose of the project) abstraction of the reality and utilized **hexagonal architecture**. In other context, that during **Event Storming** turned out to lack any **complex domain logic**, we applied **CRUD-like local architecture**. ![Architecture](docs/images/architecture-big-picture.png) -If we are talking about hexagonal architecture, it lets us separate domain and application logic from -frameworks (and infrastructure). What do we gain with this approach? Firstly, we can unit test most important -part of the application - **business logic** - usually without the need to stub any dependency. -Secondly, we create ourselves an opportunity to adjust infrastructure layer without the worry of -breaking the core functionality. In the infrastructure layer we intensively use Spring Framework -as probably the most mature and powerful application framework with an incredible test support. -More information about how we use Spring you will find [here](#spring). +If we are talking about **hexagonal architecture**, it lets us **separate domain and application logic from frameworks and infrastructure**. What do we gain with this approach? +- Firstly, we can **unit test** most important part of the application - **business logic** - usually without the need to stub any dependency. +- Secondly, we create ourselves an opportunity to **adjust infrastructure layer without the worry of breaking the core functionality**. + +In the infrastructure layer we intensively use Spring Framework as probably the most mature and powerful application framework with an incredible test support. More information about how we use Spring you will find [here](#spring). -As we already mentioned, the architecture was driven by Event Storming sessions. Apart from identifying -contexts and their complexity, we could also make a decision that we separate read and write models (CQRS). -As an example you can have a look at **Patron Profiles** and *Daily Sheets*. +As we already mentioned, the architecture was driven by **Event Storming Sessions**. Apart from identifying contexts and their complexity, we could also make a decision that we **separate read and write models** ([CQRS](https://martinfowler.com/bliki/CQRS.html)). +As an example you can have a look at *Patron Profiles* and *Daily Sheets*. ### Aggregates -Aggregates discovered during Event Storming sessions communicate with each other with events. There is -a contention, though, should they be consistent immediately or eventually? As aggregates in general -determine business boundaries, eventual consistency sounds like a better choice, but choices in software -are never costless. Providing eventual consistency requires some infrastructural tools, like message broker -or event store. That's why we could (and did) start with immediate consistency. +**Aggregates** discovered during Event Storming Sessions **communicate** with each other through **events**. There is a contention, though, should they be **consistent immediately or eventually**? As aggregates in general determine **business boundaries**, eventual consistency sounds like a better choice, but choices in software are **never costless**. Providing eventual consistency requires some infrastructural tools, like message broker or event store. That's why we could (and did) start with **immediate consistency**. -> Good architecture is the one which postpones all important decisions +> Good architecture allows major architectural decisions to be deferred -... that's why we made it easy to change the consistency model, providing tests for each option, including -basic implementations based on **DomainEvents** interface, which can be adjusted to our needs and -toolset in future. Let's have a look at following examples: +... that's why we made it **easy to change** the consistency model, providing **tests** for each option, including basic implementations based on **Domain Events** interface, which can be adjusted to our needs and tool set in future. Let's have a look at following examples: * Immediate consistency ```groovy @@ -140,15 +126,16 @@ toolset in future. Let's have a look at following examples: } boolean dailySheetIsUpdated() { - return new JdbcTemplate(datasource).query("select count(*) from holds_sheet s where s.hold_by_patron_id = ?", + return new JdbcTemplate(datasource) + .query("select count(*) from holds_sheet s where s.hold_by_patron_id = ?", [patronId.patronId] as Object[], new ColumnMapRowMapper()).get(0) .get("COUNT(*)") == 1 } ``` - _Please note that here we are just reading from database right after events are being published_ + _Please note that here we are **just reading** from database right after events are being published._ - Simple implementation of the event bus is based on Spring application events: + Simple implementation of the **event bus** is based on Spring Application Events: ```java @AllArgsConstructor public class JustForwardDomainEventPublisher implements DomainEvents { @@ -191,10 +178,9 @@ toolset in future. Let's have a look at following examples: } } ``` - _Please note that the test looks exactly the same as previous one, but now we utilized Groovy's - **PollingConditions** to perform asynchronous functionality tests_ + _Please note that the test looks exactly the same as previous one, but now we utilized Groovy's **PollingConditions** to perform **asynchronous functionality tests**._ - Sample implementation of event bus is following: + Sample implementation of **event bus** is following: ```java @AllArgsConstructor @@ -218,27 +204,15 @@ toolset in future. Let's have a look at following examples: } ``` -To clarify, we should always aim for aggregates that can handle a business operation atomically -(transactionally if you like), so each aggregate should be as independent and decoupled from other -aggregates as possible. Thus, eventual consistency is promoted. As we already mentioned, it comes -with some tradeoffs, so from the pragmatic point of view immediate consistency is also a choice. -You might ask yourself a question now: _What if I don't have any events yet?_. Well, a pragmatic -approach would be to encapsulate the communication between aggregates in a _Service-like_ class, -where you could call proper aggregates line by line explicitly. +To clarify, we should always aim for aggregates that can handle a **business operation atomically** (transactionally, if you like), so each aggregate should be as independent and decoupled from other aggregates as possible. Thus, eventual consistency is promoted. As we already mentioned, it comes with some tradeoffs, so from the pragmatic point of view immediate consistency is also a choice. + +You might ask yourself a question now: _What if I don't have any events yet?_. Well, a pragmatic approach would be to **encapsulate the communication** between aggregates in a _Service-like_ class, where you could call proper aggregates line by line explicitly. ### Events -Talking about inter-aggregate communication, we must remember that events reduce coupling, but don't remove -it completely. Thus, it is very vital to share(publish) only those events, that are necessary for other -aggregates to exist and function. Otherwise there is a threat that the level of coupling will increase -introducing **feature envy**, because other aggregates might start using those events to perform actions -they are not supposed to perform. A solution to this problem could be the distinction of domain events -and integration events, which will be described here soon. +Talking about **inter-aggregate communication**, we must remember that events reduce coupling, but don't remove it completely. Thus, it is very vital to share (**publish**) only those events that are **necessary** for other aggregates to exist and function. Otherwise there is a threat that the level of coupling will increase introducing [**feature envy**](https://refactoring.guru/smells/feature-envy), because other aggregates might start using those events to perform actions they are not supposed to perform. A solution to this problem could be the **distinction** of domain events and integration events, which will be described here soon. ### Events in Repositories -Repositories are one of the most popular design pattern. They abstract our domain model from data layer. -In other words, they deal with state. That said, a common use-case is when we pass a new state to our repository, -so that it gets persisted. It may look like so: - +Repositories are one of the most popular **design pattern**. They **abstract** our domain model from data layer. In other words, they deal with state. That said, a common use-case is when we pass a new state to our repository, so that it gets persisted. It may look like so: ```java public class BusinessService { @@ -246,44 +220,33 @@ public class BusinessService { void businessMethod(PatronId patronId) { Patron patron = patronRepository.findById(patronId); - //do sth + //do something patronRepository.save(patron); } } ``` -Conceptually, between 1st and 3rd line of that business method we change state of our Patron from A to B. -This change might be calculated by dirty checking or we might just override entire Patron state in the database. -Third option is _Let's make implicit explicit_ and actually call this state change A->B an **event**. -After all, event-driven architecture is all about promoting state changes as domain events. - -Thanks to this our domain model may become immutable and just return events as results of invoking a command like so: +Conceptually, between 1st and 3rd line of that business method we **change state** of our Patron from A to B. This change might be calculated by dirty checking or we might just override entire Patron state in the database. Third option is _Let's make implicit explicit_ and actually call this state change A->B an **event**. After all, event-driven architecture is all about promoting state changes as domain events. +Thanks to this our domain model may become **immutable** and just return events as results of invoking a command like so: ```java public BookPlacedOnHold placeOnHold(AvailableBook book) { - ... + ... } ``` And our repository might operate directly on events like so: - ```java public interface PatronRepository { - void save(PatronEvent event) { + void save(PatronEvent event); } ``` ### ArchUnit +One of the main components of a successful project is technical leadership that lets the team go in the right direction. Nevertheless, there are **tools** that can support teams in keeping the code clean and protect the architecture, so that the project won't become a **Big Ball of Mud**, and thus will be pleasant to develop and to maintain. The first option, the one we proposed, is [**ArchUnit**](https://www.archunit.org/) - a Java architecture test tool. ArchUnit lets you write unit tests of your architecture, so that it is always **consistent** with initial vision. Maven modules could be an alternative as well, but let's focus on the former. -One of the main components of a successful project is technical leadership that lets the team go in the right -direction. Nevertheless, there are tools that can support teams in keeping the code clean and protect the -architecture, so that the project won't become a Big Ball of Mud, and thus will be pleasant to develop and -to maintain. The first option, the one we proposed, is [ArchUnit](https://www.archunit.org/) - a Java architecture -test tool. ArchUnit lets you write unit tests of your architecture, so that it is always consistent with initial -vision. Maven modules could be an alternative as well, but let's focus on the former. +In terms of **hexagonal architecture**, it is essential to ensure, that we do not mix different **levels of abstraction** (hexagon levels): -In terms of hexagonal architecture, it is essential to ensure, that we do not mix different levels of -abstraction (hexagon levels): ```java @ArchTest public static final ArchRule model_should_not_depend_on_infrastructure = @@ -293,8 +256,9 @@ public static final ArchRule model_should_not_depend_on_infrastructure = .should() .dependOnClassesThat() .resideInAPackage("..infrastructure.."); -``` -and that frameworks do not affect the domain model +``` + +and that frameworks **do not affect** the domain model: ```java @ArchTest public static final ArchRule model_should_not_depend_on_spring = @@ -307,25 +271,20 @@ public static final ArchRule model_should_not_depend_on_spring = ``` ### Functional thinking -When you look at the code you might find a scent of functional programming. Although we do not follow -a _clean_ FP, we try to think of business processes as pipelines or workflows, utilizing functional style through -following concepts. +When you look at the code you might find a scent of **functional programming**. Although we do not follow a _clean FP_, we try to **think of business processes as pipelines or workflows**, utilizing functional style through following concepts. This is very power for code legibility and reuse. _Please note that this is not a reference project for FP._ #### Immutable objects -Each class that represents a business concept is immutable, thanks to which we: -* provide full encapsulation and objects' states protection, -* secure objects for multithreaded access, -* control all side effects much clearer. +Each class that represents a **business concept** is immutable, thanks to which we: +* provide full encapsulation and objects' states protection; +* secure objects for multithreaded access; +* control all side effects much clearer; #### Pure functions -We model domain operations, discovered in Design Level Event Storming, as pure functions, and declare them in -both domain and application layers in the form of Java's functional interfaces. Their implementations are placed -in infrastructure layer as ordinary methods with side effects. Thanks to this approach we can follow the abstraction -of ubiquitous language explicitly, and keep this abstraction implementation-agnostic. As an example, you could have -a look at `FindAvailableBook` interface and its implementation: +We model **domain operations**, discovered in Design Level Event Storming, as **pure functions**, and declare them in both domain and application layers in the form of **Java's functional interfaces**. Their implementations are placed in infrastructure layer as ordinary methods with side effects. Thanks to this approach we can follow the abstraction of **ubiquitous language explicitly**, and keep this **abstraction implementation-agnostic**. +As an example, you could have a look at `FindAvailableBook` interface and its implementation: ```java @FunctionalInterface public interface FindAvailableBook { @@ -363,116 +322,102 @@ class BookDatabaseRepository implements FindAvailableBook { ``` #### Type system -_Type system - like_ modelling - we modelled each domain object's state discovered during EventStorming as separate -classes: `AvailableBook`, `BookOnHold`, `CheckedOutBook`. With this approach we provide much clearer abstraction than -having a single `Book` class with an enum-based state management. Moving the logic to these specific classes brings -Single Responsibility Principle to a different level. Moreover, instead of checking invariants in every business method -we leave the role to the compiler. As an example, please consider following scenario: _you can place on hold only a book -that is currently available_. We could have done it in a following way: +_Type system - like_ modelling - we modelled each domain object's state discovered during EventStorming as separate classes: `AvailableBook`, `BookOnHold`, `CheckedOutBook`. + +With this approach we provide much clearer abstraction than having a single `Book` class with an enum-based state management. Moving the logic to these specific classes brings **Single Responsibility Principle** to a different level. Moreover, instead of checking invariants in every business method we leave the role to the compiler. + +As an example, please consider following scenario: _you can place on hold only a book that is currently available_. We could have done it in a following way: ```java public Either placeOnHold(Book book) { - if (book.status == AVAILABLE) { - ... - } + if (book.status == AVAILABLE) { + ... + } } ``` -but we use the _type system_ and declare method of following signature +but we use the _type system_ and declare method of following signature: ```java public Either placeOnHold(AvailableBook book) { - ... + ... } ``` -The more errors we discover at compile time the better. +_The more errors we discover at compile time the **better**._ + +Yet another advantage of applying such type system is that we can **represent business flows and state transitions with functions** much easier. -Yet another advantage of applying such type system is that we can represent business flows and state transitions -with functions much easier. As an example, following functions: +As an example, following functions: ``` placeOnHold: AvailableBook -> BookHoldFailed | BookPlacedOnHold cancelHold: BookOnHold -> BookHoldCancelingFailed | BookHoldCanceled ``` -are much more concise and descriptive than these: + +are much more **concise and descriptive** than these: ``` placeOnHold: Book -> BookHoldFailed | BookPlacedOnHold cancelHold: Book -> BookHoldCancelingFailed | BookHoldCanceled ``` as here we have a lot of constraints hidden within function implementations. -Moreover if you think of your domain as a set of operations (functions) that are being executed on business objects -(aggregates) you don't think of any execution model (like async processing). It is fine, because you don't have to. -Domain functions are free from I/O operations, async, and other side-effects-prone things, which are put into the -infrastructure layer. Thanks to this, we can easily test them without mocking mentioned parts. +Moreover if you think of your domain as a **set of operations** (functions) that are being **executed on business objects** (aggregates) you don't think of any execution model (like async processing). It is fine, because you don't have to. Domain functions are **free** from I/O operations, async, and other side-effects-prone things, which are put into the infrastructure layer. Thanks to this, we can **easily test** them without mocking mentioned parts. #### Monads Business methods might have different results. One might return a value or a `null`, throw an exception when something -unexpected happens or just return different objects under different circumstances. All those situations are typical -to object-oriented languages like Java, but do not fit into functional style. We are dealing with this issues -with monads (monadic containers provided by [Vavr](https://www.vavr.io)): -* When a method returns optional value, we use the `Option` monad: +unexpected happens or just return different objects under different circumstances. All those situations are typical to object-oriented languages like Java, but do not fit into functional style. We are dealing with this issues with monads (**monadic containers** provided by [Vavr](https://www.vavr.io)): +* When a method returns **optional value**, we use the `Option` monad: ```java Option findBy(BookId bookId) { ... } ``` -* When a method might return one of two possible values, we use the `Either` monad: - +* When a method might return **one of two possible values**, we use the `Either` monad: ```java Either placeOnHold(AvailableBook book) { ... } ``` -* When an exception might occur, we use `Try` monad: - +* When an **exception** might occur, we use `Try` monad: ```java Try placeOnHold(@NonNull PlaceOnHoldCommand command) { ... } ``` -Thanks to this, we can follow the functional programming style, but we also enrich our domain language and -make our code much more readable for the clients. +Thanks to this, we can follow the functional programming style, but we also **enrich our domain language** and make our code much more **readable** for the clients. #### Pattern Matching -Depending on a type of a given book object we often need to perform different actions. Series of if/else or switch/case statements -could be a choice, but it is the pattern matching that provides the most conciseness and flexibility. With the code -like below we can check numerous patterns against objects and access their constituents, so our code has a minimal dose -of language-construct noise: +Depending on a **type** of a given book object we often need to perform different **actions**. Series of if/else or switch/case statements could be a choice, but it is the pattern matching that provides the most **conciseness and flexibility**. + +With the code like below we can check numerous **patterns** against objects and access their constituents, so our code has a minimal dose of language-construct noise: ```java private Book handleBookPlacedOnHold(Book book, BookPlacedOnHold bookPlacedOnHold) { return API.Match(book).of( Case($(instanceOf(AvailableBook.class)), availableBook -> availableBook.handle(bookPlacedOnHold)), - Case($(instanceOf(BookOnHold.class)), bookOnHold -> raiseDuplicateHoldFoundEvent(bookOnHold, bookPlacedOnHold)), + Case($(instanceOf(BookOnHold.class)), bookOnHold -> raiseDuplicateHoldFoundEvent(bookOnHold, bookPlacedOnHold), Case($(), () -> book) ); } ``` ### (No) ORM -If you run `mvn dependency:tree` you won't find any JPA implementation. Although we think that ORM solutions (like Hibernate) -are very powerful and useful, we decided not to use them, as we wouldn't utilize their features. What features are -talking about? Lazy loading, caching, dirty checking. Why don't we need them? We want to have more control -over SQL queries and minimize the object-relational impedance mismatch ourselves. Moreover, thanks to relatively -small aggregates, containing as little data as it is required to protect the invariants, we don't need the -lazy loading mechanism either. -With Hexagonal Architecture we have the ability to separate domain and persistence models and test them -independently. Moreover, we can also introduce different persistence strategies for different aggregates. -In this project, we utilize both plain SQL queries and `JdbcTemplate` and use new and very promising -project called Spring Data JDBC, that is free from the JPA-related overhead mentioned before. -Please find below an example of a repository: +If you run `mvn dependency:tree` you won't find any JPA implementation. Although we think that ORM solutions (like Hibernate) are very powerful and useful, we decided not to use them, as we wouldn't utilize their features. + +What features are talking about? Lazy loading, caching, dirty checking... Why don't we need them? We want to have more **control** over SQL queries and **minimize** the object-relational impedance mismatch ourselves. Moreover, thanks to **relatively small aggregates**, containing as little data as it is required to protect the invariants, we don't need the lazy loading mechanism either. +With **Hexagonal Architecture** we have the ability to separate domain and persistence models and test them independently. Moreover, we can also introduce different persistence strategies for different aggregates. +In this project, we utilize both plain SQL queries and `JdbcTemplate` and use new and very promising project called `Spring Data JDBC`, that is free from the JPA-related overhead mentioned before. + +Please find below an example of a repository: ```java interface PatronEntityRepository extends CrudRepository { @Query("SELECT p.* FROM patron_database_entity p where p.patron_id = :patronId") PatronDatabaseEntity findByPatronId(@Param("patronId") UUID patronId); - } ``` -At the same time we propose other way of persisting aggregates, with plain SQL queries and `JdbcTemplate`: - +At the same time we propose other **way of persisting aggregates**, with plain SQL queries and `JdbcTemplate`: ```java @AllArgsConstructor class BookDatabaseRepository implements BookRepository, FindAvailableBook, FindBookOnHold { @@ -491,22 +436,15 @@ class BookDatabaseRepository implements BookRepository, FindAvailableBook, FindB new BeanPropertyRowMapper<>(BookDatabaseEntity.class), bookId.getBookId()))) .getOrElse(none()); } - ... } ``` -_Please note that despite having the ability to choose different persistence implementations for aggregates -it is recommended to stick to one option within the app/team_ +_Please note that despite having the ability to **choose different persistence implementations** for aggregates it is recommended to stick to one option within the app/team._ ### Architecture-code gap -We put a lot of attention to keep the consistency between the overall architecture (including diagrams) -and the code structure. Having identified bounded contexts we could organize them in modules (packages, to -be more specific). Thanks to this we gain the famous microservices' autonomy, while having a monolithic -application. Each package has well defined public API, encapsulating all implementation details by using -package-protected or private scopes. +We put a lot of attention to keep the **consistency** between the overall architecture (including diagrams) and the code structure. Having identified bounded contexts we could **organize** them in modules (packages, to be more specific). Thanks to this we gain the famous **microservices' autonomy**, while having a monolithic application. Each package has **well defined public API**, encapsulating all implementation details by using package-protected or private scopes. Just by looking at the package structure: - ``` └── library ├── catalogue @@ -534,29 +472,25 @@ Just by looking at the package structure: ├── model └── web ``` -you can see that the architecture is screaming that it has two bounded contexts: **catalogue** -and **lending**. Moreover, the **lending context** is built around five business objects: **book**, -**dailysheet**, **librarybranch**, **patron**, and **patronprofile**, while **catalogue** has no subpackages, -which suggests that it might be a CRUD with no complex logic inside. Please find the architecture diagram -below. + +You can see that the architecture is screaming that it has two bounded contexts: **catalogue** and **lending**. + +Moreover, the **lending context** is built around five business objects: **book**, **dailysheet**, **librarybranch**, **patron**, and **patronprofile**, while **catalogue** has no subpackages, which suggests that it might be a CRUD with no complex logic inside. + +Please find the **architecture diagram** below: ![Component diagram](docs/c4/component-diagram.png) -Yet another advantage of this approach comparing to packaging by layer for example is that in order to -deliver a functionality you would usually need to do it in one package only, which is the aforementioned -autonomy. This autonomy, then, could be transferred to the level of application as soon as we split our -_context-packages_ into separate microservices. Following this considerations, autonomy can be given away -to a product team that can take care of the whole business area end-to-end. +Yet another advantage of this approach comparing to packaging by layer for example is that in order to deliver a functionality you would usually need to do it in one package only, which is the aforementioned **autonomy**. This autonomy, then, could be transferred to the level of application as soon as we split our _context-packages_ into separate microservices. Following this considerations, autonomy can be given away to a product team that can take care of the whole business area end-to-end. ### Model-code gap In our project we do our best to reduce _model-code gap_ to bare minimum. It means we try to put equal attention -to both the model and the code and keep them consistent. Below you will find some examples. +to both the model and the code and keep them **consistent**. Below you will find some examples. #### Placing on hold ![Placing on hold](docs/images/placing_on_hold.jpg) Starting with the easiest part, below you will find the model classes corresponding to depicted command and events: - ```java @Value class PlaceOnHoldCommand { @@ -582,10 +516,7 @@ class BookHoldFailed implements PatronEvent { } ``` -We know it might not look impressive now, but if you have a look at the implementation of an aggregate, -you will see that the code reflects not only the aggregate name, but also the whole scenario of `PlaceOnHold` -command handling. Let us uncover the details: - +We know it might not look impressive now, but if you have a look at the implementation of an aggregate, you will see that the code reflects not only the aggregate name, but also the whole scenario of `PlaceOnHold` command handling. Let us uncover the details: ```java public class Patron { @@ -597,10 +528,8 @@ public class Patron { } ``` -The signature of `placeOnHold` method screams, that it is possible to place a book on hold only when it -is available (more information about protecting invariants by compiler you will find in [Type system section](#type-system)). -Moreover, if you try to place available book on hold it can **either** fail (`BookHoldFailed`) or produce some events - -what events? +The signature of `placeOnHold` method screams, that it is possible to place a book on hold only when it is available (more information about protecting invariants by compiler you will find in [Type system section](#type-system)). +Moreover, if you try to place available book on hold it can **either** fail (`BookHoldFailed`) or produce some events - what events? ```java @Value @@ -619,8 +548,10 @@ class BookPlacedOnHoldEvents implements PatronEvent { return new BookPlacedOnHoldEvents(bookPlacedOnHold.getPatronId(), bookPlacedOnHold, Option.none()); } - public static BookPlacedOnHoldEvents events(BookPlacedOnHold bookPlacedOnHold, MaximumNumberOfHoldsReached maximumNumberOfHoldsReached) { - return new BookPlacedOnHoldEvents(bookPlacedOnHold.patronId, bookPlacedOnHold, Option.of(maximumNumberOfHoldsReached)); + public static BookPlacedOnHoldEvents events(BookPlacedOnHold bookPlacedOnHold, + MaximumNumberOfHoldsReached maximumNumberOfHoldsReached) { + return new BookPlacedOnHoldEvents(bookPlacedOnHold.patronId, bookPlacedOnHold, + Option.of(maximumNumberOfHoldsReached)); } public List normalize() { @@ -629,18 +560,14 @@ class BookPlacedOnHoldEvents implements PatronEvent { } ``` -`BookPlacedOnHoldEvents` is a container for `BookPlacedOnHold` event, and - if patron has 5 book placed on hold already - -`MaximumNumberOfHoldsReached` (please mind the `Option` monad). You can see now how perfectly the code reflects -the model. +`BookPlacedOnHoldEvents` is a container for `BookPlacedOnHold` event, and - if patron has 5 book placed on hold already - `MaximumNumberOfHoldsReached` (please mind the `Option` monad). You can see now how perfectly **the code reflects the model**. -It is not everything, though. In the picture above you can also see a big rectangular yellow card with rules (policies) -that define the conditions that need to be fulfilled in order to get the given result. All those rules are implemented -as functions **either** allowing or rejecting the hold: +It is not everything, though. In the picture above you can also see a big rectangular yellow card with rules (policies) that define the conditions that need to be fulfilled in order to get the given result. All those rules are implemented as functions **either** allowing or rejecting the hold: ![Restricted book policy](docs/images/placing-on-hold-policy-restricted.png) ```java -PlacingOnHoldPolicy onlyResearcherPatronsCanHoldRestrictedBooksPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> { - if (toHold.isRestricted() && patron.isRegular()) { +PlacingOnHoldPolicy onlyResearcherPatronsCanHoldRestrictedBooksPolicy = (AvailableBook bookToHold, Patron patron, HoldDuration holdDuration) -> { + if (bookToHold.isRestricted() && patron.isRegular()) { return left(Rejection.withReason("Regular patrons cannot hold restricted books")); } return right(new Allowance()); @@ -648,47 +575,40 @@ PlacingOnHoldPolicy onlyResearcherPatronsCanHoldRestrictedBooksPolicy = (Availab ``` ![Overdue checkouts policy](docs/images/placing-on-hold-policy-overdue.png) - ```java -PlacingOnHoldPolicy overdueCheckoutsRejectionPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> { - if (patron.overdueCheckoutsAt(toHold.getLibraryBranch()) >= OverdueCheckouts.MAX_COUNT_OF_OVERDUE_RESOURCES) { - return left(Rejection.withReason("cannot place on hold when there are overdue checkouts")); +PlacingOnHoldPolicy overdueCheckoutsRejectionPolicy = (AvailableBook bookToHold, Patron patron, HoldDuration holdDuration) -> { + if (patron.overdueCheckoutsAt(bookToHold.getLibraryBranch()) >= OverdueCheckouts.MAX_COUNT_OF_OVERDUE_RESOURCES) { + return left(Rejection.withReason("Cannot place book on hold when there are overdue checkouts")); } return right(new Allowance()); }; ``` ![Max number of holds policy](docs/images/placing-on-hold-policy-max.png) - ```java -PlacingOnHoldPolicy regularPatronMaximumNumberOfHoldsPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> { +PlacingOnHoldPolicy regularPatronMaximumNumberOfHoldsPolicy = (AvailableBook bookToHold, Patron patron, HoldDuration holdDuration) -> { if (patron.isRegular() && patron.numberOfHolds() >= PatronHolds.MAX_NUMBER_OF_HOLDS) { - return left(Rejection.withReason("patron cannot hold more books")); + return left(Rejection.withReason("Patron cannot hold more books")); } return right(new Allowance()); }; ``` ![Open ended hold policy](docs/images/placing-on-hold-policy-open-ended.png) - ```java -PlacingOnHoldPolicy onlyResearcherPatronsCanPlaceOpenEndedHolds = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> { +PlacingOnHoldPolicy onlyResearcherPatronsCanPlaceOpenEndedHolds = (AvailableBook bookToHold, Patron patron, HoldDuration holdDuration) -> { if (patron.isRegular() && holdDuration.isOpenEnded()) { - return left(Rejection.withReason("regular patron cannot place open ended holds")); + return left(Rejection.withReason("Regular patron cannot place open ended holds")); } return right(new Allowance()); }; ``` #### Spring -Spring Framework seems to be the most popular Java framework ever used. Unfortunately it is also quite common -to overuse its features in the business code. What you find in this project is that the domain packages -are fully focused on modelling business problems, and are free from any DI, which makes it easy to -unit-test it which is invaluable in terms of code reliability and maintainability. It does not mean, -though, that we do not use Spring Framework - we do. Below you will find some details: -- Each bounded context has its own independent application context. It means that we removed the runtime -coupling, which is a step towards extracting modules (and microservices). How did we do that? Let's have -a look: +Spring Framework seems to be the most popular Java framework ever used. Unfortunately it is also quite common to **overuse** its features in the business code. What you find in this project is that the **domain packages are fully focused on modelling business problems**, and are free from any DI, which makes it easy to unit-test it which is invaluable in terms of **code reliability and maintainability**. + +It does not mean, though, that we do not use Spring Framework - we do. Below you will find some details: +- Each bounded context has its own independent application context. It means that we removed the runtime coupling, which is a step towards extracting modules (and microservices). How did we do that? Let's have a look: ```java @SpringBootConfiguration @EnableAutoConfiguration @@ -703,17 +623,13 @@ a look: } } ``` -- As you could see above, we also try not to use component scan wherever possible. Instead we utilize -`@Configuration` classes where we define module specific beans in the infrastructure layer. Those -configuration classes are explicitly declared in the main application class. + +- As you could see above, we also try not to use component scan wherever possible. Instead we utilize `@Configuration` classes where we define module specific beans in the infrastructure layer. Those configuration classes are **explicitly declared** in the main application class. ### Tests -Tests are written in a BDD manner, expressing stories defined with Example Mapping. -It means we utilize both TDD and Domain Language discovered with Event Storming. +Tests are written in a BDD manner, **expressing stories** defined with Example Mapping. It means we utilize both TDD and Domain Language discovered with Event Storming. -We also made an effort to show how to create a DSL, that enables to write -tests as if they were sentences taken from the domain descriptions. Please -find an example below: +We also made an effort to show how to create a DSL, that enables to write tests as if they were **sentences** taken from the domain descriptions. Please find an example below: ```groovy def 'should make book available when hold canceled'() { @@ -730,14 +646,11 @@ def 'should make book available when hold canceled'() { availableBook.version == bookOnHold.version } ``` -_Please also note the **when** block, where we manifest the fact that books react to -cancellation event_ +_Please also note the **when** block, where we manifest the fact that books react to cancellation event._ ## How to contribute -The project is still under construction, so if you like it enough to collaborate, just let us -know or simply create a Pull Request. - +The project is still under construction, so if you like it enough to collaborate, just let us know or simply create a Pull Request. ## How to Build @@ -754,10 +667,9 @@ You can run the library app by simply typing the following: $ mvn spring-boot:run ... ... -2019-04-03 15:55:39.162 INFO 18957 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator' -2019-04-03 15:55:39.425 INFO 18957 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' -2019-04-03 15:55:39.428 INFO 18957 --- [ main] io.pillopl.library.LibraryApplication : Started LibraryApplication in 5.999 seconds (JVM running for 23.018) - +2019-04-03 15:55:39.162 INFO 18957 --- [main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator' +2019-04-03 15:55:39.425 INFO 18957 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path +2019-04-03 15:55:39.428 INFO 18957 --- [main] io.pillopl.library.LibraryApplication : Started LibraryApplication in 5.999 seconds (JVM running for 23.018) ``` ### Build a Jar package @@ -806,8 +718,7 @@ If everything goes well, you can access the following services at given location * http://localhost:9090 - Prometheus dashboard * http://localhost:3000 - Grafana dashboard -In order to see some metrics, you must create a dashboard. Go to `Create` -> `Import` and select attached `jvm-micrometer_rev8.json`. File has been pulled from -`https://grafana.com/grafana/dashboards/4701`. +In order to see some metrics, you must create a dashboard. Go to `Create` -> `Import` and select attached `jvm-micrometer_rev8.json`. File has been pulled from `https://grafana.com/grafana/dashboards/4701`. Please note application will be run with `local` Spring profile to setup some initial data. diff --git a/src/main/java/io/pillopl/library/catalogue/Book.java b/src/main/java/io/pillopl/library/catalogue/Book.java index a7b4261..e7e524a 100644 --- a/src/main/java/io/pillopl/library/catalogue/Book.java +++ b/src/main/java/io/pillopl/library/catalogue/Book.java @@ -23,7 +23,6 @@ class Book { } } - @Value class Title { @@ -35,7 +34,6 @@ class Title { } this.title = title.trim(); } - } @Value @@ -49,4 +47,4 @@ class Author { } this.name = name.trim(); } -} \ No newline at end of file +} diff --git a/src/main/java/io/pillopl/library/catalogue/BookInstance.java b/src/main/java/io/pillopl/library/catalogue/BookInstance.java index 49cefeb..31519ad 100644 --- a/src/main/java/io/pillopl/library/catalogue/BookInstance.java +++ b/src/main/java/io/pillopl/library/catalogue/BookInstance.java @@ -20,6 +20,5 @@ class BookInstance { static BookInstance instanceOf(Book book, BookType bookType) { return new BookInstance(book.getBookIsbn(), new BookId(UUID.randomUUID()), bookType); - } } diff --git a/src/main/java/io/pillopl/library/catalogue/BookType.java b/src/main/java/io/pillopl/library/catalogue/BookType.java index f8811b1..2b40f33 100644 --- a/src/main/java/io/pillopl/library/catalogue/BookType.java +++ b/src/main/java/io/pillopl/library/catalogue/BookType.java @@ -3,4 +3,3 @@ public enum BookType { Restricted, Circulating } - diff --git a/src/main/java/io/pillopl/library/catalogue/Catalogue.java b/src/main/java/io/pillopl/library/catalogue/Catalogue.java index 7ad61d3..69352ea 100644 --- a/src/main/java/io/pillopl/library/catalogue/Catalogue.java +++ b/src/main/java/io/pillopl/library/catalogue/Catalogue.java @@ -37,7 +37,4 @@ private BookInstance saveAndPublishEvent(BookInstance bookInstance) { domainEvents.publish(new BookInstanceAddedToCatalogue(bookInstance)); return bookInstance; } - - } - diff --git a/src/main/java/io/pillopl/library/catalogue/CatalogueDatabase.java b/src/main/java/io/pillopl/library/catalogue/CatalogueDatabase.java index 691db4b..b39e8c0 100644 --- a/src/main/java/io/pillopl/library/catalogue/CatalogueDatabase.java +++ b/src/main/java/io/pillopl/library/catalogue/CatalogueDatabase.java @@ -45,7 +45,6 @@ Option findBy(ISBN isbn) { } } - } @Data diff --git a/src/main/java/io/pillopl/library/catalogue/CatalogueDatabaseConfig.java b/src/main/java/io/pillopl/library/catalogue/CatalogueDatabaseConfig.java index 920a117..c86cc72 100644 --- a/src/main/java/io/pillopl/library/catalogue/CatalogueDatabaseConfig.java +++ b/src/main/java/io/pillopl/library/catalogue/CatalogueDatabaseConfig.java @@ -38,4 +38,4 @@ DataSource dataSource() { .addScript("create_catalogue_book.sql") .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/io/pillopl/library/catalogue/ISBN.java b/src/main/java/io/pillopl/library/catalogue/ISBN.java index cee0cee..cd831c8 100644 --- a/src/main/java/io/pillopl/library/catalogue/ISBN.java +++ b/src/main/java/io/pillopl/library/catalogue/ISBN.java @@ -16,6 +16,5 @@ class ISBN { throw new IllegalArgumentException("Wrong ISBN!"); } this.isbn = isbn.trim(); - } } diff --git a/src/main/java/io/pillopl/library/commons/commands/BatchResult.java b/src/main/java/io/pillopl/library/commons/commands/BatchResult.java index 2ac67bb..72fbbfe 100644 --- a/src/main/java/io/pillopl/library/commons/commands/BatchResult.java +++ b/src/main/java/io/pillopl/library/commons/commands/BatchResult.java @@ -2,4 +2,4 @@ public enum BatchResult { FullSuccess, SomeFailed -} \ No newline at end of file +} diff --git a/src/main/java/io/pillopl/library/commons/commands/Result.java b/src/main/java/io/pillopl/library/commons/commands/Result.java index 3273922..1f6be7f 100644 --- a/src/main/java/io/pillopl/library/commons/commands/Result.java +++ b/src/main/java/io/pillopl/library/commons/commands/Result.java @@ -2,4 +2,4 @@ public enum Result { Success, Rejection -} \ No newline at end of file +} diff --git a/src/main/java/io/pillopl/library/commons/events/publisher/StoreAndForwardDomainEventPublisher.java b/src/main/java/io/pillopl/library/commons/events/publisher/StoreAndForwardDomainEventPublisher.java index 7db4fa5..d1832c3 100644 --- a/src/main/java/io/pillopl/library/commons/events/publisher/StoreAndForwardDomainEventPublisher.java +++ b/src/main/java/io/pillopl/library/commons/events/publisher/StoreAndForwardDomainEventPublisher.java @@ -7,7 +7,6 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.transaction.annotation.Transactional; - @AllArgsConstructor public class StoreAndForwardDomainEventPublisher implements DomainEvents { diff --git a/src/main/resources/create_catalogue_book.sql b/src/main/resources/create_catalogue_book.sql index f1235c0..bd579a4 100644 --- a/src/main/resources/create_catalogue_book.sql +++ b/src/main/resources/create_catalogue_book.sql @@ -4,7 +4,6 @@ CREATE TABLE IF NOT EXISTS catalogue_book ( title VARCHAR(100) NOT NULL, author VARCHAR(100) NOT NULL); - CREATE TABLE IF NOT EXISTS catalogue_book_instance ( id INTEGER IDENTITY PRIMARY KEY, isbn VARCHAR(100) NOT NULL, @@ -12,5 +11,3 @@ CREATE TABLE IF NOT EXISTS catalogue_book_instance ( CREATE SEQUENCE catalogue_book_seq; CREATE SEQUENCE catalogue_book_instance_seq; - - diff --git a/src/main/resources/create_patron_db.sql b/src/main/resources/create_patron_db.sql index 239a2cf..9d82c00 100644 --- a/src/main/resources/create_patron_db.sql +++ b/src/main/resources/create_patron_db.sql @@ -1,5 +1,17 @@ -CREATE TABLE IF NOT EXISTS patron_database_entity (id INTEGER IDENTITY PRIMARY KEY, patron_type VARCHAR(100) NOT NULL, patron_id UUID UNIQUE); +CREATE TABLE IF NOT EXISTS patron_database_entity ( + id INTEGER IDENTITY PRIMARY KEY, + patron_type VARCHAR(100) NOT NULL, + patron_id UUID UNIQUE); -CREATE TABLE IF NOT EXISTS hold_database_entity (id INTEGER IDENTITY PRIMARY KEY, book_id UUID NOT NULL, patron_id UUID NOT NULL, library_branch_id UUID NOT NULL, patron_database_entity INTEGER NOT NULL, till TIMESTAMP NOT NULL); +CREATE TABLE IF NOT EXISTS hold_database_entity ( + id INTEGER IDENTITY PRIMARY KEY, + book_id UUID NOT NULL, patron_id UUID NOT NULL, + library_branch_id UUID NOT NULL, + patron_database_entity INTEGER NOT NULL, + till TIMESTAMP NOT NULL); -CREATE TABLE IF NOT EXISTS overdue_checkout_database_entity (id INTEGER IDENTITY PRIMARY KEY, book_id UUID NOT NULL, patron_id UUID NOT NULL, library_branch_id UUID NOT NULL, patron_database_entity INTEGER NOT NULL); +CREATE TABLE IF NOT EXISTS overdue_checkout_database_entity ( + id INTEGER IDENTITY PRIMARY KEY, + book_id UUID NOT NULL, patron_id UUID NOT NULL, + library_branch_id UUID NOT NULL, + patron_database_entity INTEGER NOT NULL); diff --git a/src/main/resources/create_sheets_db.sql b/src/main/resources/create_sheets_db.sql index bfee527..14d5c81 100644 --- a/src/main/resources/create_sheets_db.sql +++ b/src/main/resources/create_sheets_db.sql @@ -9,7 +9,6 @@ id INTEGER IDENTITY PRIMARY KEY, checked_out_at_branch UUID, checkout_till TIMESTAMP); - CREATE TABLE IF NOT EXISTS holds_sheet ( id INTEGER IDENTITY PRIMARY KEY, book_id UUID NOT NULL, @@ -25,4 +24,3 @@ id INTEGER IDENTITY PRIMARY KEY, CREATE SEQUENCE holds_sheet_seq; CREATE SEQUENCE checkouts_sheet_seq; - diff --git a/src/test/groovy/io/pillopl/library/ModularArchitectureTest.java b/src/test/groovy/io/pillopl/library/ModularArchitectureTest.java index 961cd71..eb80575 100644 --- a/src/test/groovy/io/pillopl/library/ModularArchitectureTest.java +++ b/src/test/groovy/io/pillopl/library/ModularArchitectureTest.java @@ -38,6 +38,4 @@ public class ModularArchitectureTest { .should() .dependOnClassesThat() .resideInAPackage("..lending.."); - - -} \ No newline at end of file +} diff --git a/src/test/groovy/io/pillopl/library/catalogue/AddBookToCatalogueTest.groovy b/src/test/groovy/io/pillopl/library/catalogue/AddBookToCatalogueTest.groovy index 7b8b9aa..9caa179 100644 --- a/src/test/groovy/io/pillopl/library/catalogue/AddBookToCatalogueTest.groovy +++ b/src/test/groovy/io/pillopl/library/catalogue/AddBookToCatalogueTest.groovy @@ -23,7 +23,6 @@ class AddBookToCatalogueTest extends Specification { then: result.isSuccess() result.get() == Result.Success - } def 'should add a new book instance to catalogue'() { @@ -38,7 +37,6 @@ class AddBookToCatalogueTest extends Specification { result.get() == Result.Success and: 1 * domainEvents.publish(_ as BookInstanceAddedToCatalogue) - } def 'should reject adding a new book instance to catalogue when book isbn does not exist'() { @@ -53,7 +51,6 @@ class AddBookToCatalogueTest extends Specification { result.get() == Result.Rejection and: 0 * domainEvents.publish(_ as BookInstanceAddedToCatalogue) - } def 'should fail when adding a book if database fails'() { @@ -81,13 +78,11 @@ class AddBookToCatalogueTest extends Specification { void databaseWorks() { catalogueDatabase.saveNew(_ as Book) >> null catalogueDatabase.saveNew(_ as BookInstance) >> null - } void databaseDoesNotWork() { catalogueDatabase.saveNew(_ as Book) >> { (new IllegalStateException()) } catalogueDatabase.saveNew(_ as BookInstance) >> { (new IllegalStateException()) } - } void thereIsBookWith(String isbn) { diff --git a/src/test/groovy/io/pillopl/library/catalogue/BookTitleAuthorISBNTest.groovy b/src/test/groovy/io/pillopl/library/catalogue/BookTitleAuthorISBNTest.groovy index 829866e..ff53dba 100644 --- a/src/test/groovy/io/pillopl/library/catalogue/BookTitleAuthorISBNTest.groovy +++ b/src/test/groovy/io/pillopl/library/catalogue/BookTitleAuthorISBNTest.groovy @@ -9,7 +9,6 @@ class BookTitleAuthorISBNTest extends Specification { Title title = new Title(" to trim ") expect: title.title == "to trim" - } def "author should be trimmed"() { @@ -68,5 +67,3 @@ class BookTitleAuthorISBNTest extends Specification { thrown(IllegalArgumentException) } } - - diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 0bb1726..0a24fb4 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -10,4 +10,4 @@ - \ No newline at end of file +