Layered Architecture
Learning Objectives
Self Study
As you read through the resources below try to answer the following questions:
- In a sentence or two, briefly summarise what we mean by ‘software architecture’?
- Why is a layered architecture useful? Think in particular about complex software systems that are live in production for a long time (and hence technology evolves!), and where maintainability, testability and reliability are important, where changes are constantly being made and where lots of developers in different teams are making changes at the same time
- How do interfaces help to decouple the layers in an application and why is this decoupling useful?
- Can you think of a project you’ve worked on where you used a layered architecture? If so, did it help? If you’ve never used a layered architecture before, can you think of a project that could be improved if refactored to do so?
Resources
- Introduction to software architecture - read sections Introduction to software architecture, Importance of software architecture and Software architecture design (the rest discusses architecture design patterns so feel free to read that too!)
- Good overview of layered architecture - focus on why layered architecture is useful, familiarise yourself with the UI, business logic and database layers, and look at the example application
Exercises
✍️Exercise 1.1
For a previous project you have worked on, describe the tiers of architecture used. Draw a diagram of the architecture tiers used and the key components and software they use (e.g. React front end, Java Service layer, Hibernate DAO and Postgres DB).
Consider a social media application where users can write posts on a forum. Consider the process of a user adding a new post - which tier of the application would the code for the following actions live?
Handling the user interaction to navigate to the ’new post’ screen
Posting the callback to refresh the page when the user saves the post
Validating that the post doesn’t contain any banned words or phrases after the user hits save
Scanning the contents of the post to add suitable tags to make it appear in relavent search results
Loading a draft post that was previously saved
Storing the the post long term
Dependencies and Dependency Injection
Learning Objectives
Self Study
As you read through the resources below try to answer the following questions:
- What do we mean by a ‘dependency’ in a Java class?
- What is Inversion of Control (IoC) and how does it relate to Dependency Injection (DI)?
- Can you give an example of IoC that isn’t DI?
- What are some potential issues with Java classes that have dependencies but don’t use DI. Think about testability, maintainability and tight-coupling for large code bases that evolve over time.
- How does DI help solve the above issues?
- What is constructor injection and how is it different to method injection?
Resources
- Overview of dependency injection - ignore section on interface injection
- A more general discussion of Inversion of Control and how Dependency Injection is an example -Note some of the ideas here can be a little hard to understand in the abstract - focusing on the examples can be a good way to see the concepts in action
Exercises
✍️Exercise 2.1
Refactor the code below to use constructor based dependency injection:
public static void main(String[] args) {
Car car = new Car();
car.start();
}
static class Car {
Engine engine = new Engine();
void start() {
engine.rev();
System.out.println("Car Started");
}
}
static class Engine {
Gears gears = new Gears();
Cambelt cambelt = new Cambelt();
void rev() {
gears.initialise();
cambelt.initialise();
}
}
static class Gears {
Set<Cog> cogs = Set.of(new Cog(), new Cog(), new Cog());
void initialise() {
cogs.forEach(cog -> cog.spin());
// Initialisation logic
}
}
static class Cog {
void spin() {
// Spin logic
}
}
static class Cambelt {
void initialise() {
// Initialisation logic
}
}Reflections
Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.
- Suppose we wanted to refactor our car such that it supported multiple different types of engine. How might we do it?
- How would unit testing the Car class be easier after it was refactored to use DI? Remember the unit tests for Car should not need to also test Engine, Gears, Cogs etc
- Why in this example (and in general) do we prefer constructor injection to setter injection?
The Service Layer
Learning Objectives
Self Study
As you read through the resources below try to answer the following questions:
- Where should simple business logic live? e.g. for a Car class with an Engine dependency, in which class should the code to validate that it’s engine must be less than 10 years old live?
- When should standalone service classes be used?
- Should service classes be interfaces or concrete classes? Always? Why? Think about reusability, maintainability, testability.
- What do dependency injection frameworks do and how do they help with dependency injection of services? Note we aren’t going to be using any DI frameworks yet, but it’s good to understand the motivation for them.
Resources
- Overview of service / business logic layer - ignore the non java parts
- Comparison of Service and Database Access Layers (as initially it may seem they are the same)
- Motivation for using DI in the service layer by Guice, a dependency injection framework
✍️Exercise 3.1
Build a Payment processor that needs to work with different providers:
- Create a
PaymentProcessorinterface with methods:boolean processPayment(double amount)boolean refund(String transactionId)String getProviderName()
- Create THREE implementations (in reality each implementation would contain the code to call to the relevant API, but we’ll skip that part here!):
StripePaymentProcessor:processPayment: prints “Processing £X via Stripe…”- Returns true (simulate success)
PayPalPaymentProcessor:processPayment: prints “Processing £X via PayPal…”- Returns true (simulate success)
DummyPaymentProcessor:- Doesn’t actually process anything
- Just tracks method calls for testing
- Stores: List of amounts processed, List of refund IDs
- Create
CheckoutService:- Has
PaymentProcessordependency which should be injected in the constructor - Method:
processCheckout(List<Double> amounts)- Calculates total
- Uses
PaymentProcessorto charge - Returns success/failure
- Has
- Create a main method demonstrating:
- Production with Stripe
PaymentProcessor stripe = new StripePaymentProcessor();CheckoutService checkoutService = new CheckoutService(stripe);checkoutService.processCheckout(cart);
- Production with PayPal
PaymentProcessor paypal = new PayPalPaymentProcessor();CheckoutService checkoutService2 = new CheckoutService(paypal);checkoutService2.processCheckout(cart);
- Production with Stripe
- Testing
DummyPaymentProcessor dummyProcessor = new DummyPaymentProcessor();CheckoutService dummyService = new CheckoutService(testProcessor);dummyService.processCheckout(cart);- Verify: dummyProcessor.getProcessedAmounts() contains expected values
Reflections
Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.
- Did CheckoutService change when switching processors?
- Could you add a new processor without changing CheckoutService?
- Suppose we updated the implementations above to call the real APIs
- What would we need to change if Stripe’s API changes?
- How does having the specific dummy test implementation simplify testing?