Last active
January 30, 2024 11:27
-
-
Save gushakov/ef8c48494aa2a5fb87b621495c373177 to your computer and use it in GitHub Desktop.
Example of a presenter in Clean DDD
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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