Skip to content

Instantly share code, notes, and snippets.

@gushakov
Last active January 30, 2024 11:27
Show Gist options
  • Save gushakov/ef8c48494aa2a5fb87b621495c373177 to your computer and use it in GitHub Desktop.
Save gushakov/ef8c48494aa2a5fb87b621495c373177 to your computer and use it in GitHub Desktop.
Example of a presenter in Clean DDD
import lombok.Getter;
import javax.transaction.Transactional;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Currency;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
class InvalidDomainObjectError extends RuntimeException {
}
class InvoiceGenerationError extends RuntimeException {
}
class PersistenceOperationError extends RuntimeException {
}
@Getter
class Money {
BigDecimal amount;
String currencyCode;
private Money(BigDecimal amount, String currencyCode) {
this.amount = Optional.ofNullable(amount).orElseThrow(InvalidDomainObjectError::new);
this.currencyCode = Optional.ofNullable(currencyCode).orElseThrow(InvalidDomainObjectError::new);
}
public static Money of(BigDecimal amount, String currencyCode) {
return new Money(amount, currencyCode);
}
}
class PersonId {
UUID uuid;
private PersonId(UUID uuid) {
this.uuid = Optional.ofNullable(uuid).orElseThrow(InvalidDomainObjectError::new);
}
public static PersonId of(UUID customerUuid) {
return new PersonId(customerUuid);
}
}
@Getter
class Person {
PersonId id;
String firstName;
String lastName;
}
class Invoice {
PersonId customerId;
@Getter
Money invoiceAmount;
@Getter
Instant invoiceDateTime;
private Invoice(PersonId customerId, Money invoiceAmount, Instant invoiceDateTime) {
this.customerId = Optional.ofNullable(customerId).orElseThrow(InvalidDomainObjectError::new);
this.invoiceAmount = Optional.ofNullable(invoiceAmount).orElseThrow(InvalidDomainObjectError::new);
this.invoiceDateTime = Optional.ofNullable(invoiceDateTime).orElseThrow(InvalidDomainObjectError::new);
}
public static Invoice of(PersonId customerId, Money invoiceAmount, Instant invoiceDateTime) {
return new Invoice(customerId, invoiceAmount, invoiceDateTime);
}
}
interface PersistenceGatewayOutputPort {
void rollback();
void saveInvoice(Invoice invoice);
Person loadPersonById(PersonId personId);
}
interface GenerateInvoiceInputPort {
void generateInvoiceForCustomer(UUID customerUuid, BigDecimal invoiceAmount, String currencyCode);
}
interface GenerateInvoicePresenterOutputPort {
void presentError(Exception e);
void presentInvalidInputError(InvalidDomainObjectError e);
void presentPersistenceOperationError(PersistenceOperationError e);
void presentInvoiceGenerationError(InvoiceGenerationError e);
void presentResultOfGeneratingInvoice(Invoice invoice, Person customer);
}
class GenerateInvoiceUseCase implements GenerateInvoiceInputPort {
private GenerateInvoicePresenterOutputPort presenter;
private PersistenceGatewayOutputPort gateway;
@Transactional
@Override
public void generateInvoiceForCustomer(UUID customerUuid, BigDecimal amount, String currencyCode) {
PersonId customerId;
Money invoiceAmount;
Invoice invoice;
Person customer;
boolean success = false;
try {
// process input arguments
try {
invoiceAmount = Money.of(amount, currencyCode);
customerId = PersonId.of(customerUuid);
} catch (InvalidDomainObjectError e) {
presenter.presentInvalidInputError(e);
return;
}
// perform business logic: generate new invoice
try {
invoice = Invoice.of(customerId, invoiceAmount, Instant.now());
} catch (InvoiceGenerationError e) {
presenter.presentInvoiceGenerationError(e);
return;
}
// save invoice
try {
gateway.saveInvoice(invoice);
} catch (PersistenceOperationError e) {
presenter.presentPersistenceOperationError(e);
return;
}
// we need customer's personal information for presentation,
// so we load it here
try {
customer = gateway.loadPersonById(customerId);
} catch (PersistenceOperationError e) {
presenter.presentPersistenceOperationError(e);
return;
}
} catch (Exception e) {
// catch any errors we may have forgotten to handle,
// especially needed during development and debugging
presenter.presentError(e);
return;
} finally {
// rollback the transaction if the use case did not succeed
if (!success) {
gateway.rollback();
}
}
// call presenter to present successful outcome of the use case
presenter.presentResultOfGeneratingInvoice(invoice, customer);
}
}
class ViewModel {
public void setError(String localizedMessage) {
}
public void setFormattedInvoiceDate(String invoiceDate) {
}
public void setFormattedInvoiceAmount(String invoiceAmount) {
}
public void setCustomerName(String setCustomerName) {
}
}
class GenerateInvoiceWebPresenter implements GenerateInvoicePresenterOutputPort {
private static final ZoneId ZONE_ID = ZoneId.of("Europe/Zurich");
private static final Locale LOCALE = new Locale("fr");
private static final DateTimeFormatter INVOICE_DATE_FORMATTER = DateTimeFormatter.ofPattern("MM-dd-yyyy", LOCALE);
private static final String CUSTOMER_NAME_TEMPLATE = "%s, %s";
// get the view-model for this presentation
ViewModel viewModel = new ViewModel();
@Override
public void presentError(Exception e) {
viewModel.setError(e.getLocalizedMessage());
}
@Override
public void presentInvalidInputError(InvalidDomainObjectError e) {
viewModel.setError(e.getLocalizedMessage());
}
@Override
public void presentPersistenceOperationError(PersistenceOperationError e) {
viewModel.setError(e.getLocalizedMessage());
}
@Override
public void presentInvoiceGenerationError(InvoiceGenerationError e) {
viewModel.setError(e.getLocalizedMessage());
}
@Override
public void presentResultOfGeneratingInvoice(Invoice invoice, Person customer) {
try {
// set billing date in view-model formatted with specific timezone and locale formatter
viewModel.setFormattedInvoiceDate(invoice.getInvoiceDateTime().atZone(ZONE_ID).format(INVOICE_DATE_FORMATTER));
// set invoice amount in view-model formatted using appropriate currency format
viewModel.setFormattedInvoiceAmount(formatMoneyAmount(invoice.getInvoiceAmount()));
// set the name of the customer on the invoice
viewModel.setCustomerName(CUSTOMER_NAME_TEMPLATE.formatted(customer.lastName, customer.firstName));
} catch (Exception e) {
// do not propagate any exceptions to the caller
presentError(e);
}
}
private String formatMoneyAmount(Money amount) {
Currency currency = Currency.getInstance(amount.getCurrencyCode());
NumberFormat format = new DecimalFormat();
format.setCurrency(currency);
return "%s %s".formatted(format.format(amount.getAmount()), format.getCurrency().getSymbol());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment