Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Refactoring With Types

DZone 's Guide to

Refactoring With Types

Learn more about Java refactoring with Types.

· Java Zone ·
Free Resource

Refactoring with types

Learn more about refactoring with types in Java

In this post, we will look at some refactoring techniques using types. Types can be used to represent the domain in a fine-grained, well-defined way. Additionally, types can be used to incorporate business rules in a manner that ensures code correctness. This enables us to write simple and elegant unit tests to ensure code correctness.

You may also like: What Is Refactoring?

Refactoring With Types

Recently, while reviewing code, I came across the following class:

public class OrderLine {
  private int quantity;
  private Double unitPrice;
  private Double listPrice;
  private Double tax;
  private Double charge;
  //Rest of the implementation
}


The code above is a classic example of a code smell called primitive obsession.

All the above parameters are represented using numbers. However, are they just numbers? Is UnitPrice interchangeable with ListPrice or Tax?

In domain-driven design, these are indeed distinct things and not just numbers. Ideally, we would like to have specific types to represent these concepts.

The first level of refactoring is to create simple wrapper types for these classes:

public class ListPrice {
    private ListPrice() {
    }
    private @Getter Double listPrice;
    public ListPrice(Double listPrice) {
        setListPrice(listPrice);
    }
    private void setListPrice(Double listPrice) {
        Objects.requireNonNull(listPrice, "list price can not be null");
        if (listPrice < 0) {
            throw new IllegalArgumentException("Invalid list price: "+listPrice);
        }
        this.listPrice = listPrice;
    }
}



public class UnitPrice {
    private UnitPrice() {
    }
    private @Getter Double unitPrice;
    public unitPrice(Double unitPrice) {
        setUnitPrice(unitPrice);
    }
    private void setUnitPrice(Double unitPrice) {
        Objects.requireNonNull(unitPrice, "unit price can not be null");
        if (unitPrice < 0) {
            throw new IllegalArgumentException("Invalid unit price: "+unitPrice);
        }
        this.unitPrice = unitPrice;
    }
}


This serves as a good starting point. We now have conceptual constructs for these. Any business rules required for a construct can now be wired within these constructs rather than being implemented in the container OrderLine class.

However, if we observe that there is duplicate code to check that the listPrice and unitPrice are not be null or non-negative, this check would most probably be applied to quantity, tax, and charge as well.

Hence, it makes sense to create a Type, which represents a non-negative number concept.

public class NonNegativeDouble {
    private @Getter Double value;
    public NonNegativeDouble(Double value){
        this.setValue(value);
    }
    private void setValue(Double value) {
        Objects.requireNonNull(value,"Value cannot be null");
        if(value < 0){
            throw new IllegalArgumentException("Value has to be positive");
        }
      this.value = value;
    }
}


Now, we can safely refactor the UnitPrice and ListPrice classes to use this new construct of a non-negative double.

public class UnitPrice {
    private UnitPrice() {
    }
    private
    @Getter
    NonNegativeDouble unitPrice;
    public UnitPrice(NonNegativeDouble unitPrice) {
        setUnitPrice(unitPrice);
    }
    private void setUnitPrice(NonNegativeDouble unitPrice) {
        this.unitPrice = unitPrice;
    }
}


A simple test to validate the non-negative constraints for UnitPrice, use the following code:

@Unroll
class UnitPriceSpec extends Specification {
    def "#text creation of Unit Price object with value - (#unitPriceValue)"() {
        given:
        def unitPrice
        when:
        boolean isExceptionThrown = false
        try {
            unitPrice = new UnitPrice(new NonNegativeDouble(unitPriceValue))
        } catch (Exception ex) {
            isExceptionThrown = true
        }
        then:
        assert isExceptionThrown == isExceptionExpected
        where:
        text        |   unitPriceValue      |   isExceptionExpected
        'Valid'     |   120                 |   false
        'Valid'     |   12.34               |   false
        'Valid'     |   0.8989              |   false
        'Valid'     |   12567652365.67667   |   false
        'Invalid'   |   0                   |   false
        'Invalid'   |   0.00000             |   false
        'Invalid'   |   -23.5676            |   true
        'Invalid'   |   -23478687           |   true
        'Invalid'   |   null                |   true
    }
}


Although this showcases a simple use case for refactoring using types, it applies to a lot of constructs which are modeled as primitive types, like Email, Names, Currency, Ranges, and Date and Time.

Refactoring: Using Types to Make the Illegal States Unrepresentable

Another example of refactoring that provides a lot of value is to make illegal state unrepresentable in a domain model. As an example, consider the following Java class:

public class CustomerContact {
private @Getter EmailContactInfo emailContactInfo;
private @Getter PostalContactInfo postalContactInfo;

public CustomerContact(EmailContactInfo emailContactInfo, 
PostalContactInfo postalContactInfo){
setEmailContactInfo(emailContactInfo);
setPostalContactInfo(postalContactInfo);
}
private void setEmailContactInfo(EmailContactInfo emailContactInfo){
Objects.requireNonNull(emailContactInfo,"Email Contact 
                Info cannot be null");
this.emailContactInfo = emailContactInfo;
}
private void setPostalContactInfo(PostalContactInfo postalContactInfo){
Objects.requireNonNull(postalContactInfo,"Postal Contact 
                Info cannot be null");
this.postalContactInfo = postalContactInfo;
}

}


Based on the previous refactoring, we have already extracted the domain-level constructs  EmailContactInfo and PostalContactInfo. These are true domain-level constructs as opposed to just being strings.

Let's assume a simple business rule, which states: "A customer contact must have either email contact information or postal contact information."

This implies that there should be at least one of either the EmailContactInfo or the CustomerContactInfo. Both can also be present. However, our current implementation requires both to be present.

In order to apply the business rule, a first attempt might look like this:

public class CustomerContact {
    private @Getter Optional<EmailContactInfo> emailContactInfo;
    private @Getter Optional<PostalContactInfo> postalContactInfo;

    public CustomerContact(PersonName name, Optional<EmailContactInfo> emailContactInfo,
                           Optional<PostalContactInfo> postalContactInfo){
        setEmailContactInfo(emailContactInfo);
        setPostalContactInfo(postalContactInfo);
    }
    private void setEmailContactInfo(Optional<EmailContactInfo> emailContactInfo){
        this.emailContactInfo = emailContactInfo;
    }
    private void setPostalContactInfo(Optional<PostalContactInfo> postalContactInfo){
        this.postalContactInfo = postalContactInfo;
    }

}


Now, we have gone too far the other way. The rule requires that the CustomerContact should have at least one of email or postal contact. However, with the current implementation, it is possible for the CustomerContact to not have any of them.

Simplifying the business rule leads to the following
Customer Contact = Email Contact or Postal Contact or Both Email and Postal Contact

In a functional language, such conditions can be designed using sum types. However, in languages like Java, there is no first-class support for these constructs. There are libraries like JavaSealedUnions, which provide support for Sum  and Union  types in java.

Using JavaSealedUnions, we can implement the business rule as follows:


public abstract class CustomerContact implements Union2<EmailContact, PostalContact> {

    public abstract boolean valid();

    public static CustomerContact email(String emailAddress) {
        return new EmailContact(emailAddress);
    }

    public static CustomerContact postal(String postalAddress) {
        return new PostalContact(postalAddress);
    }
}

class EmailContact extends CustomerContact {

    private final String emailAddress;


    EmailContact(String emailAddress) {
        this.emailAddress = emailAddress;        
    }

    public boolean valid() {
        return /* some logic here */
    }

    public void continued(Consumer<EmailContact> continuationLeft, Consumer<PostalContact> continuationRight) {
        continuationLeft.call(value);
    }

    public <T> T join(Function<EmailContact, T> mapLeft, Function<PostalContact, T> mapRight) {
        return mapLeft.call(value);
    }
}

class PostalContact extends CustomerContact {

    private final String address;


    PostalContact(String address) {
        this.address = address;

    }

    public boolean valid() {
        return /* some logic here */
    }

    public void continued(Consumer<EmailContact> continuationLeft, Consumer<PostalContact> continuationRight) {
        continuationRight.call(value);
    }

    public <T> T join(Function<EmailContact, T> mapLeft, Function<PostalContact, T> mapRight) {
        return mapRight.call(value);
    }
}


// Example

CustomerContact customerContact = getCustomerContact();
if (customerContact.valid()) {
    customerContact.continued(customerContactService::byEmail(), customerContactService::byPostalAddress())
}


This post shows some of the ways we were thinking in terms of how Types can help in having a cleaner design. Types also help to avoid ambiguity around business rules. The approaches shown above can also be used in other scenarios to either capture allowed states or success and failure cases.

Further Reading

What Is Refactoring?

Nine Steps of Learning By Refactoring

[DZone Refcard] Refactoring Patterns

Topics:
refactoring

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}