Skip to content

Instantly share code, notes, and snippets.

@xpepper
Last active April 26, 2024 08:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save xpepper/74a81232ef59dde3c4684c960acb482a to your computer and use it in GitHub Desktop.
Save xpepper/74a81232ef59dde3c4684c960acb482a to your computer and use it in GitHub Desktop.
Thomas Pierrain & Bruno Boucard - How To Distill The Core Domain From Your Legacy App (Live Coding)

Thomas Pierrain & Bruno Boucard - How To Distill The Core Domain From Your Legacy App (Live Coding)

Authors:

DDD is somehow clear in theory, but difficult to apply when coding, especially when you have to deal with legacy code.

They start from the Train Reservation Kata but then build a "legacy mess".

Rules

  • Don't reserve more than 70% of the seats in the train ("train capacity")
  • All seats of a reservation must be in the same coach (don't split families!)

Domain terms

  • Train
  • Train Id
  • Coach
  • Seat
  • Reservation attempt
  • Reservation fulfilled
  • Reservation failure
  • Reservation
  • To book
  • Booking reference

Bounded context

Four contexts:

  • Seat Reservation - our bounded context (Train-Train startup!)
  • Train Data
  • Booking Reference
  • Train Booking

Seat Reservation

The main bounded context, from our startup Train-Train ("reserve seat nicely!"): to reserve seats on a train.

Our context is "downstream" with respect to the other contexts, which are out of our control: we can't change them, but we break our system if they decide to change. => we depend on those contexts.

Train Data

Provides the train topology (the number of coaches, of seats, their reservation status). Need to call this service before attempting a reservation to check if there are available seats.

Booking Reference

Need to call this service first to have a valid "booking reference id", a unique id to use to then issue the seat reservation

Train Booking

Need to call this service to reserve seats on a train


Demo of the web API.

There's a bug! And they're loosing money!

They have a problem! They are facing recurrent penalties from the operator.

Every time they call any external service, they pay a fee... but they pay too much! Why is that?

=> Introducing legacy code!

Before touching legacy code you've to have a test harness in place!

They wrote 3 ATs:

  • reserve 3 seats on a 1-coach train => happy path!
  • cannot reserve 2 seats when they overwhelm the 70% limit rule
  • "all seats in the same couch" rule (=> but there's a bug here!)

Let's fix the bug! => write a failing test and than fix it!

ASP.net, controller

The entry point of the application is WebTicketManager.Reserve(train_id, number_of_seats)

The AT covers the WebTicketManager class, which represent the system as a black-box: all the other external systems are fakes, the test invoke the Reserve method on WebTicketManager.

See https://github.com/42skillz/liveCoding-LegacyTrain/blob/master/TrainTrain.Test/Acceptance/TrainTrainSystemShould.cs

Train Data service and Train Booking service has been merged in a single service.

Code review of WebTicketManager.Reserve method to understand it before trying to fix the bug.

  • A continuous testing tool used!
  • Seems they never saw that code before! :)
  • They do some minor refactoring while talking (rename variables, formatting...)

How do I start on a legacy code with a bug nailed down by a failing test?
If I change code and the test is still broken I don't know what caused the red now: my last change or the actual bug?

They ignore the failing test and start to refactor the code to let it become more clear and easier to change.

In order to be able to refactor the code you have to:

  1. have a safety net in case you slip and introduce a regression. That safety net should be your actual test harness, your existing suite of tests in place.

  2. try to discover and understand how the application works, what the code is doing. Have a discussion with the domain experts, have a code review, etc.

  3. cleaning the deck: clean up the code to get more confidence with the code (e.g. renaming, extracting logic, remove useless code, useless comments, etc...). Get comfortable with the code, step by step. - see also https://www.slideshare.net/brunoboucard/how-to-test-untestable-code

we'll do many extract methods and move methods, to extract behaviours and find new domain types

=> move the local variable declaration close to its usage! This then ease the method extraction.

Observed smells:

  • unused variables
  • bad variable names
  • variables declared long before being used
  • non-idiomatic code (e.g. getter method instead of expression body)
  • more classes in a single file (not idiomatic)
  • mixed concerns, one layer leaking in another, coupling (JSON in the domain objects...)
  • comments

Json parsing in the middle of the Train constructor...

Tip: extract method and make it static to spot dependencies with the original class and see clearly how to remove them.

Adapter type: TrainService is a proxy toward an external system (it issues the HTTP requests): we change it to return an instance of Train instead of the raw JSON!


Train is still NOT a domain class, because has no behaviour at all!

(They switch role and pass the keyboard!)

We still could not find the concept of:

  • Reservation attempt
  • Coach

=> Anemic Domain: it describe part of the domain knowledge (some terms have a corresponding class) but the behaviour is not in the domain classes, it's elsewhere.

Extract method, move method

We don't have the concept of coach in the code, this is why we cannot enforce the "all seats in the same couch" rule! Let's work to have this concept represented in the code...

They're moving behaviour from the WebTicketManager to the Train class (to fight the Anemic Domain).

Concept of "Reservation Attempt" => the first phase of a reservation.
Let's create a ReservationAttempt class.
Move some behaviour on ReservationAttempt (isFulfilled, AssignBookingReference).

Next, introduce the concept of Coach in the code.
Starts with a unit test on Train (TrainShould.Expose_Coaches).

It's strange I never see the test go actually green...
Moreover, the test seems a bit of an excuse to introduce the Coach type: it's written and then forgotten while we add the Coach type, start moving logic in it and use it in the Train class. 😕

DDD Concept: Aggregate

DDD Concept: Aggregate - a collection of objects (entities or value objects) treated as a conceptual whole.
Train is an aggregate, the root of the aggregate: the train aggregates a list of Coaches, which in turn contain a list of Seats.

Coach is an aggregate? No (at least not in this example): aggregate needs invariants.
In this example there's a business rule that says "In reserving seat, do not exceed 70% of the overall train capacity". This rule must be enforce by the Train, and cannot be enforced by a Coach: this is a responsibility of the train, cannot be taken by the coach because a coach does not have the knowledge that its train has instead.

Hexagonal Architecture

Architectural pattern, a friendly pattern for the DDD ecosystem :)

It has many benefits:

  • embrace change
    • it's a plugin-oriented pattern (switch implementations easily)
  • help to protect your domain code
    • from the technical stuff (infrastructure code, delivery mechanism, other external systems, ...)
  • enforce testability (because of the 'pluggability', you can inject a fake/stub/mock in place of the real external systems)

Help to protect your domain code

There are just two sides: inside and outside. Inside is your domain code. Outside is all the infrastructure, the external world (HTTP, DB, ... )

Barriers to protect your domain! Infrastructure will not jeopardize the domain.

Dependencies

You have basically two assemblies: one for the outside, one for the domain inside. The dependencies go always toward the inside: the outside can refer to the inside, but the opposite is never true.

This is done using the Dependency Inversion Principle (DIP), or better, with "Configurable Dependencies" plugged into the system, to be able to access the outside world (e.g. repositories).

Ports and Adapters

Ports belong to the domain: in Java / C# ports are modelled as interfaces of the language. They are entry points to the domain (e.g. IReserveSeats), to enter or to go out the domain hexagon. They should have names that reflect their role in the domain, should speak the language of the domain.

Adapters are in the infrastructure code. They are like "gateways", to enter or to leave the hexagon. They are real "adapters" (in the GOF sense): their responsibility is to translate from one world into another. E.g. TrainDataServiceAdapter, BookingReferenceServiceAdapter, ...

Ports and Adapters to go in and to go out the domain.

A three-step initialization

The key ring metaphor:

You need to follow this order when bootstrapping your application:

  1. First, instantiate the "I need to go out" adapters (e.g. TrainDataServiceAdapter, BookingReferenceServiceAdapter, ...), in the "main" of your application

  2. Then, you instantiate the hexagon, which will wrap and aggregates the adapters "to go out"

  3. Finally, you instantiate the "I need to enter" adapters, the one you need to enter and talk with the hexagon.

At the end of the day, all you'll keep in your hands will be the "entry point" adapter: the external world will not talk directly to the hexagon, it will need to talk to the "I need to go in" adapter, which behind the scene will talk to the hexagon to fulfill the request.

Final wrap-up

  1. Talk to your domain experts! Business experts help a lot in understand names and concepts of the domain, and so help in refactoring the codebase. EventStorming sessions can help too!

  2. Don't be daunted by legacy code!

  3. Apply "Extract & Move" refactoring move. And do look for code smells: for example "Feature Envy" is very important to smell and fix.


Example JSON

{
    "seats": {
        "1A": {
            "booking_reference": "",
            "coach": "A",
            "seat_number": "1"
        },
        "2A": {
            "booking_reference": "",
            "coach": "A",
            "seat_number": "2"
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment