docs: update service layer

This commit is contained in:
Ilkka Seppälä
2024-05-20 13:03:23 +03:00
parent 2a789cf65a
commit d656295d3c
6 changed files with 181 additions and 208 deletions
+167 -126
View File
@@ -3,104 +3,78 @@ title: Service Layer
category: Architectural
language: en
tag:
- Data access
- API design
- Business
- Decoupling
- Enterprise patterns
- Layered architecture
---
## Also known as
* Application Facade
## Intent
Service Layer is an abstraction over domain logic. It defines application's boundary with a layer of services that
establishes a set of available operations and coordinates the application's response in each operation.
Encapsulate business logic in a distinct layer to promote separation of concerns and to provide a well-defined API for the presentation layer.
## Explanation
Typically applications require different kinds of interfaces to the data they store and the logic they implement.
Despite their different purposes, these interfaces often need common interactions with the application to access and
manipulate its data and invoke its business logic. Encoding the logic of the interactions separately in each module
causes a lot of duplication. It's better to centralize building the business logic inside single Service Layer to avoid
these pitfalls.
Real world example
Real-world example
> We are writing an application that tracks wizards, spellbooks and spells. Wizards may have spellbooks and spellbooks
may have spells.
> Consider a large restaurant where orders are taken by waitstaff and then sent to different kitchen sections (e.g., grill, salad, dessert). Each section specializes in a part of the meal, but the waitstaff don't interact directly with the kitchen staff. Instead, all orders go through a head chef who coordinates the workflow. The head chef acts like the service layer, handling the business logic (order coordination) and providing a unified interface for the waitstaff (presentation layer) to interact with the kitchen (data access layer).
In plain words
> Service Layer is an abstraction over application's business logic.
> A pattern that encapsulates business logic into a distinct layer to promote separation of concerns and provide a clear API for the presentation layer.
Wikipedia says
> Service layer is an architectural pattern, applied within the service-orientation design paradigm, which aims to
organize the services, within a service inventory, into a set of logical layers. Services that are categorized into
a particular layer share functionality. This helps to reduce the conceptual overhead related to managing the service
inventory, as the services belonging to the same layer address a smaller set of activities.
> Service layer is an architectural pattern, applied within the service-orientation design paradigm, which aims to organize the services, within a service inventory, into a set of logical layers. Services that are categorized into a particular layer share functionality. This helps to reduce the conceptual overhead related to managing the service inventory, as the services belonging to the same layer address a smaller set of activities.
**Programmatic Example**
The example application demonstrates interactions between a client `App` and a service `MagicService` that allows
interaction between wizards, spellbooks and spells. The service is implemented with 3-layer architecture
The example application demonstrates interactions between a client `App` and a service `MagicService` that allows interaction between wizards, spellbooks and spells. The service is implemented with 3-layer architecture
(entity, dao, service).
For this explanation we are looking at one vertical slice of the system. Let's start from the entity layer and look at
`Wizard` class. Other entities not shown here are `Spellbook` and `Spell`.
For this explanation we are looking at one vertical slice of the system. Let's start from the entity layer and look at `Wizard` class. Other entities not shown here are `Spellbook` and `Spell`.
```java
@Entity
@Table(name = "WIZARD")
@Getter
@Setter
public class Wizard extends BaseEntity {
@Id
@GeneratedValue
@Column(name = "WIZARD_ID")
private Long id;
@Id
@GeneratedValue
@Column(name = "WIZARD_ID")
private Long id;
private String name;
private String name;
@ManyToMany(cascade = CascadeType.ALL)
private Set<Spellbook> spellbooks;
@ManyToMany(cascade = CascadeType.ALL)
private Set<Spellbook> spellbooks;
public Wizard() {
spellbooks = new HashSet<>();
}
public Wizard() {
spellbooks = new HashSet<>();
}
public Wizard(String name) {
this();
this.name = name;
}
public Wizard(String name) {
this();
this.name = name;
}
public Long getId() {
return id;
}
public void addSpellbook(Spellbook spellbook) {
spellbook.getWizards().add(this);
spellbooks.add(spellbook);
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Spellbook> getSpellbooks() {
return spellbooks;
}
public void setSpellbooks(Set<Spellbook> spellbooks) {
this.spellbooks = spellbooks;
}
public void addSpellbook(Spellbook spellbook) {
spellbook.getWizards().add(this);
spellbooks.add(spellbook);
}
@Override
public String toString() {
return name;
}
@Override
public String toString() {
return name;
}
}
```
@@ -109,29 +83,29 @@ Above the entity layer we have DAOs. For `Wizard` the DAO layer looks as follows
```java
public interface WizardDao extends Dao<Wizard> {
Wizard findByName(String name);
Wizard findByName(String name);
}
public class WizardDaoImpl extends DaoBaseImpl<Wizard> implements WizardDao {
@Override
public Wizard findByName(String name) {
Transaction tx = null;
Wizard result;
try (var session = getSessionFactory().openSession()) {
tx = session.beginTransaction();
var criteria = session.createCriteria(persistentClass);
criteria.add(Restrictions.eq("name", name));
result = (Wizard) criteria.uniqueResult();
tx.commit();
} catch (Exception e) {
if (tx != null) {
tx.rollback();
}
throw e;
@Override
public Wizard findByName(String name) {
Transaction tx = null;
Wizard result;
try (var session = getSessionFactory().openSession()) {
tx = session.beginTransaction();
var criteria = session.createCriteria(persistentClass);
criteria.add(Restrictions.eq("name", name));
result = (Wizard) criteria.uniqueResult();
tx.commit();
} catch (Exception e) {
if (tx != null) {
tx.rollback();
}
throw e;
}
return result;
}
return result;
}
}
```
@@ -140,56 +114,56 @@ Next we can look at the Service Layer, which in our case consists of a single `M
```java
public interface MagicService {
List<Wizard> findAllWizards();
List<Wizard> findAllWizards();
List<Spellbook> findAllSpellbooks();
List<Spellbook> findAllSpellbooks();
List<Spell> findAllSpells();
List<Spell> findAllSpells();
List<Wizard> findWizardsWithSpellbook(String name);
List<Wizard> findWizardsWithSpellbook(String name);
List<Wizard> findWizardsWithSpell(String name);
List<Wizard> findWizardsWithSpell(String name);
}
public class MagicServiceImpl implements MagicService {
private final WizardDao wizardDao;
private final SpellbookDao spellbookDao;
private final SpellDao spellDao;
private final WizardDao wizardDao;
private final SpellbookDao spellbookDao;
private final SpellDao spellDao;
public MagicServiceImpl(WizardDao wizardDao, SpellbookDao spellbookDao, SpellDao spellDao) {
this.wizardDao = wizardDao;
this.spellbookDao = spellbookDao;
this.spellDao = spellDao;
}
public MagicServiceImpl(WizardDao wizardDao, SpellbookDao spellbookDao, SpellDao spellDao) {
this.wizardDao = wizardDao;
this.spellbookDao = spellbookDao;
this.spellDao = spellDao;
}
@Override
public List<Wizard> findAllWizards() {
return wizardDao.findAll();
}
@Override
public List<Wizard> findAllWizards() {
return wizardDao.findAll();
}
@Override
public List<Spellbook> findAllSpellbooks() {
return spellbookDao.findAll();
}
@Override
public List<Spellbook> findAllSpellbooks() {
return spellbookDao.findAll();
}
@Override
public List<Spell> findAllSpells() {
return spellDao.findAll();
}
@Override
public List<Spell> findAllSpells() {
return spellDao.findAll();
}
@Override
public List<Wizard> findWizardsWithSpellbook(String name) {
var spellbook = spellbookDao.findByName(name);
return new ArrayList<>(spellbook.getWizards());
}
@Override
public List<Wizard> findWizardsWithSpellbook(String name) {
var spellbook = spellbookDao.findByName(name);
return new ArrayList<>(spellbook.getWizards());
}
@Override
public List<Wizard> findWizardsWithSpell(String name) {
var spell = spellDao.findByName(name);
var spellbook = spell.getSpellbook();
return new ArrayList<>(spellbook.getWizards());
}
@Override
public List<Wizard> findWizardsWithSpell(String name) {
var spell = spellDao.findByName(name);
var spellbook = spell.getSpellbook();
return new ArrayList<>(spellbook.getWizards());
}
}
```
@@ -211,17 +185,84 @@ And finally we can show how the client `App` interacts with `MagicService` in th
wizardsWithSpell.forEach(w -> LOGGER.info("{} has 'Fireball'", w.getName()));
```
The program output:
```
INFO [2024-05-20 10:00:24,548] com.iluwatar.servicelayer.app.App: Enumerating all wizards
INFO [2024-05-20 10:00:24,550] com.iluwatar.servicelayer.app.App: Aderlard Boud
INFO [2024-05-20 10:00:24,550] com.iluwatar.servicelayer.app.App: Anaxis Bajraktari
INFO [2024-05-20 10:00:24,551] com.iluwatar.servicelayer.app.App: Xuban Munoa
INFO [2024-05-20 10:00:24,551] com.iluwatar.servicelayer.app.App: Blasius Dehooge
INFO [2024-05-20 10:00:24,551] com.iluwatar.servicelayer.app.App: Enumerating all spellbooks
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Book of Orgymon
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Book of Aras
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Book of Kritior
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Book of Tamaex
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Book of Idores
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Book of Opaen
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Book of Kihione
INFO [2024-05-20 10:00:24,554] com.iluwatar.servicelayer.app.App: Enumerating all spells
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Ice dart
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Invisibility
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Stun bolt
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Confusion
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Darkness
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Fireball
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Enchant weapon
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Rock armour
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Light
INFO [2024-05-20 10:00:24,558] com.iluwatar.servicelayer.app.App: Bee swarm
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Haste
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Levitation
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Magic lock
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Summon hell bat
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Water walking
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Magic storm
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Entangle
INFO [2024-05-20 10:00:24,559] com.iluwatar.servicelayer.app.App: Find wizards with spellbook 'Book of Idores'
INFO [2024-05-20 10:00:24,560] com.iluwatar.servicelayer.app.App: Xuban Munoa has 'Book of Idores'
INFO [2024-05-20 10:00:24,560] com.iluwatar.servicelayer.app.App: Find wizards with spell 'Fireball'
INFO [2024-05-20 10:00:24,562] com.iluwatar.servicelayer.app.App: Aderlard Boud has 'Fireball'
```
## Class diagram
![alt text](./etc/service-layer.png "Service Layer")
![Service Layer](./etc/service-layer.png "Service Layer")
## Applicability
Use the Service Layer pattern when
* You want to encapsulate domain logic under API
* You need to implement multiple interfaces with common logic and data
* Use when you need to separate business logic from presentation logic.
* Ideal for applications with complex business rules and workflows.
* Suitable for projects requiring a clear API for the presentation layer.
## Known Uses
* Java EE applications where Enterprise JavaBeans (EJB) are used to implement the service layer.
* Spring Framework applications using the @Service annotation to denote service layer classes.
* Web applications that need to separate business logic from controller logic.
## Consequences
Benefits:
* Promotes code reuse by encapsulating business logic in one place.
* Enhances testability by isolating business logic.
* Improves maintainability and flexibility of the application.
Trade-offs:
* May introduce additional complexity by adding another layer to the application.
* Can result in performance overhead due to the extra layer of abstraction.
## Related Patterns
* [Facade](https://java-design-patterns.com/patterns/facade/): Simplifies interactions with complex subsystems by providing a unified interface.
* [DAO (Data Access Object)](https://java-design-patterns.com/patterns/dao/): Often used together with the Service Layer to handle data persistence.
* [MVC (Model-View-Controller)](https://java-design-patterns.com/patterns/model-view-controller/): The Service Layer can be used to encapsulate business logic in the model component.
## Credits
* [Core J2EE Patterns: Best Practices and Design Strategies](https://amzn.to/4cAbDap)
* [Patterns of Enterprise Application Architecture](https://amzn.to/3WfKBPR)
* [Spring in Action](https://amzn.to/4asnpSG)
* [Martin Fowler - Service Layer](http://martinfowler.com/eaaCatalog/serviceLayer.html)
* [Patterns of Enterprise Application Architecture](https://www.amazon.com/gp/product/0321127420/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0321127420&linkCode=as2&tag=javadesignpat-20&linkId=d9f7d37b032ca6e96253562d075fcc4a)
@@ -93,7 +93,7 @@ public abstract class DaoBaseImpl<E extends BaseEntity> implements Dao<E> {
@Override
public E merge(E entity) {
Transaction tx = null;
E result = null;
E result;
try (var session = getSessionFactory().openSession()) {
tx = session.beginTransaction();
result = (E) session.merge(entity);
@@ -33,12 +33,16 @@ import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* Spell entity.
*/
@Entity
@Table(name = "SPELL")
@Getter
@Setter
public class Spell extends BaseEntity {
private String name;
@@ -60,30 +64,6 @@ public class Spell extends BaseEntity {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Spellbook getSpellbook() {
return spellbook;
}
public void setSpellbook(Spellbook spellbook) {
this.spellbook = spellbook;
}
@Override
public String toString() {
return name;
@@ -38,12 +38,16 @@ import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* Spellbook entity.
*/
@Entity
@Table(name = "SPELLBOOK")
@Getter
@Setter
public class Spellbook extends BaseEntity {
@Id
@@ -69,38 +73,6 @@ public class Spellbook extends BaseEntity {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Wizard> getWizards() {
return wizards;
}
public void setWizards(Set<Wizard> wizards) {
this.wizards = wizards;
}
public Set<Spell> getSpells() {
return spells;
}
public void setSpells(Set<Spell> spells) {
this.spells = spells;
}
public void addSpell(Spell spell) {
spell.setSpellbook(this);
spells.add(spell);
@@ -35,12 +35,16 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* Wizard entity.
*/
@Entity
@Table(name = "WIZARD")
@Getter
@Setter
public class Wizard extends BaseEntity {
@Id
@@ -62,30 +66,6 @@ public class Wizard extends BaseEntity {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Spellbook> getSpellbooks() {
return spellbooks;
}
public void setSpellbooks(Set<Spellbook> spellbooks) {
this.spellbooks = spellbooks;
}
public void addSpellbook(Spellbook spellbook) {
spellbook.getWizards().add(this);
spellbooks.add(spellbook);
@@ -122,7 +122,7 @@ class MagicServiceImplTest {
}
@Test
void testFindWizardsWithSpell() throws Exception {
void testFindWizardsWithSpell() {
final var wizards = Set.of(
mock(Wizard.class),
mock(Wizard.class),