Heart and Soul - part 1
I dug out a really old book over the last weekend, from 1993, 1Peter Coad and Jill Nicola’s “Object-Oriented Programming”.
This 555 page (+ index) book (and it included a 3.5inch disk), has the blurb on the back:
Real Programming… not just language syntax.
And…
Experience OOP first-hand:
- Learn how to “object think”…via engaging, real-world examples
- Learn how to program in C++ and Smalltalk…with parallel examples.
- Apply reuse strategies…for current reuse and future reuse.
- Apply advanced features…C++ templates…and Smalltalk model-view-controller
It’s a book about Object-Oriented Analysis and Design (OOAD), with examples in Smalltalk and C++.
OOAD was a process developed by Coad and Yourdon, and differentiated itself from “Structured Analysis and Design”, which is what I learned way back 30 years ago.
But, it’s more than that, it has handy language summaries, where they managed to condense Smalltalk into just 31 pages, and remarkably, C++ into just 10!
I think I first bought this book in my original Smalltalk-phase, somewhere around 1998, when I was trying to understand how to design Smalltalk systems, it’s fairly unusual, the chapters alternate between C++ and Smalltalk implementations of the “real-world examples”.
There are a number of interesting principles in the book, written early on in the “object-oriented era”, some of these are still relevant, and worth digging into.
The “heart and soul” principle”
From page 230 of the book:
Make an extensive list of classes in a problem domain. Then select a small number of classes…ones that together represent the heart and soul, the core of the problem domain under consideration. Do OOA and OOD on these first. Build them. Demonstrate working results. Then go for more.
This is really about identifying what makes an application important to the business, this is also the hallmark of the “Hexagonal Architecture”, the central hexagon is the business logic, and this will normally be the “heart and soul” of the system.
I’ve run a number of “code-reading” sessions, getting engineers to read and understand codebases they’re unfamiliar with, appreciate what makes code easy and hard to understand, and identify patterns in the code, I’ll write more about this at some point, but one of the things I discuss, is identifying the core parts of the a service, as a way to understand what it does.
This is like reverse-engineeering the “heart and soul” of the problem domain, starting with the premise that the code has a clear domain purpose, and working out what it is.
An example - UK Government Pay Ledger
The trick to learning from reading code, is to pick unfamiliar code-bases, I’ll start with https://github.com/alphagov/pay-ledger.
The description is “Provide aggregate reporting on payment events”, and the README has “This is a bare bones Dropwizard App for the Pay Ledger microservice.”
So, probably makes sense to the business…
It’s using Dropwizard, and I’m fairly familiar with how a Dropwizard application is structured, but it’s just a Java application, so there should be a “main” entry point.
$ find src -name "*.java" | xargs grep main
src/main/java/uk/gov/pay/ledger/app/LedgerApp.java: public static void main(String[] args) throws Exception {
Looking in the LedgerApp
class, there are the following methods:
public class LedgerApp extends Application<LedgerConfig> {
public static void main(String[] args) throws Exception {}
@Override
public void initialize(Bootstrap<LedgerConfig> bootstrap) {}
@Override
public void run(LedgerConfig config, Environment environment) {}
}
main
just executes LedgerApp
, initialize
sets up some bundles, and the
run
method is starting the main Dropwizard application, by registering some
Jersey resources, which are REST endpoints.
EventResource
The first registered resource the EventResource
:
environment.jersey().register(injector.getInstance(EventResource.class));
Looking at the EventResource
class:
@Path("/v1/event")
@Produces(APPLICATION_JSON)
public class EventResource {
private final EventDao eventDao;
@Inject
public EventResource(EventDao eventDao) {
this.eventDao = eventDao;
}
@Path("/{eventId}")
@GET
public Event getEvent(@PathParam("eventId") Long eventId) {
return eventDao.getById(eventId)
.orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
}
}
It exposes an endpoint GET /v1/event/{eventId}
and looking at the code, it
uses an EventDao
object to find the event by the URL provided ID, and returns
it, or NOT_FOUND
.
So, I have a core entity Event
.
TransactionResource
The second registered resource is the TransactionResource
:
environment.jersey().register(injector.getInstance(TransactionResource.class));
Again, looking at the TransactionResource
:
@Path("/v1/transaction")
@Produces(APPLICATION_JSON)
public class TransactionResource {
private final TransactionService transactionService;
@Inject
public TransactionResource(TransactionService transactionService) {
this.transactionService = transactionService;
}
@Path("/{transactionExternalId}")
@GET
public TransactionView getById(@PathParam("transactionExternalId") String transactionExternalId,
@QueryParam("account_id") String gatewayAccountId,
@QueryParam("override_account_id_restriction") Boolean overrideAccountRestriction,
@QueryParam("transaction_type") TransactionType transactionType,
@QueryParam("parent_external_id") String parentTransactionExternalId) {}
@Path("/")
@GET
public TransactionSearchResponse search(@Valid
@BeanParam TransactionSearchParams searchParams,
@QueryParam("override_account_id_restriction") Boolean overrideAccountRestriction,
@QueryParam("account_id") String gatewayAccountId,
@Context UriInfo uriInfo) {}
@Path("{transactionExternalId}/event")
@GET
public TransactionEventResponse events(@PathParam("transactionExternalId") String transactionExternalId,
@QueryParam("gateway_account_id") @NotEmpty String gatewayAccountId,
@QueryParam("include_all_events") boolean includeAllEvents,
@Context UriInfo uriInfo) {}
@Path("/{parentTransactionExternalId}/transaction")
@GET
public TransactionsForTransactionResponse getTransactionsForParentTransaction(@PathParam("parentTransactionExternalId") String parentTransactionExternalId,
@QueryParam("gateway_account_id") @NotEmpty String gatewayAccountId
) {}
}
This is a significantly bigger resource, exposing four different endpoints:
GET /v1/transaction/{transactionExternalId}
GET /v1/transaction/
GET /v1/transaction/{transactionExternalId}/event
GET /v1/transaction/{parentTransactionExternalId}/transaction
And it delegates to a TransactionService
to fetch these, there’s a lot of flexibility in the querying of transactions, which means I have my second core entity Transaction
.
These make sense for a service with a description of “Provide aggregate reporting on payment event”, clearly the EventResource
exposes these payment events and the TransactionResource
provides the reporting, which explains the flexibility in the querying API.
There are two types of classes returned by the endpoints TransactionView
and 3 Response
classes, TransactionSearchResponse
, TransactionEventResponse
and TransactionsForTransactionResponse
.
Back to main
Continuing with the main
setup:
environment.jersey().register(injector.getInstance(HealthCheckResource.class));
environment.healthChecks().register("sqsQueue", injector.getInstance(SQSHealthCheck.class));
if(config.getQueueMessageReceiverConfig().isBackgroundProcessingEnabled()) {
environment.lifecycle().manage(injector.getInstance(QueueMessageReceiver.class));
}
From this, there’s a Healthchecks endpoint, which exposes the state of the healthchecks, and specific sqsQueue check, this is a giveaway that this service is receiving or publishing data to AWS SQS.
Finally, there is a check to see if “background processing is enabled”, and if so, it starts a QueueMessageReceiver
, with the sqsQueue above, it I can infer that it’s receiving data via SQS.
Class summary so far
A quick check indicates there are 61 classes in this application, and I’ve identified about a quarter of them:
- EventResource, Event and EventDao
- TransactionResource, Transaction, TransactionService, TransactionView, TransactionSearchResponse, TransactionEventResponse and TransactionsForTransactionResponse
- HealthCheckResource
- SQSHealthCheck
- QueueMessageReceiver
At this point, the mental model I have involves the Event
, Transaction
and the QueueMessageReceiver
.
In part two, I’ll sketch out the relationship between these.
-
Peter Coad and Edward Yourdon. 1991. Object-Oriented Design. Yourdon Press, Upper Saddle River, NJ, USA. ↩