Use Your Type System; Write Less Code 17 th April 2015 @SamirTalwar
Jul 28, 2015
Use Your Type System;Write Less Code
17th April 2015@SamirTalwar
So I’ve got this website.
It’s got a lot of code.
But a little while ago, I started to think it’s not as good as I thought when we were first getting started.
So I got to work.
Here’s what I found.
Readability
Readability
Tell me what’s more readable.
public Stream<Integer> searchForProperties( boolean renting, int monthlyBudget, double latitude, double longitude, double distanceFromCentre) { ... }
public Stream<PropertyId> abc( PurchasingType def, Budget ghi, Coordinate jkl, Radius mno) { ... }
Readability
Readability readable /ˈriːdəb(ə)l/ adjective
1. able to be read or deciphered; legible. “a code which is readable by a computer” synonyms: legible, easy to read, decipherable, easily deciphered, clear, intelligible, understandable, comprehensible, easy to understand “the inscription is still perfectly readable” antonyms: illegible, indecipherable 2. easy or enjoyable to read. “a marvellously readable book”
Readability readable /ˈriːdəb(ə)l/ adjective
1. able to be read or deciphered; legible. “a code which is readable by a computer” synonyms: legible, easy to read, decipherable, easily deciphered, clear, intelligible, understandable, comprehensible, easy to understand “the inscription is still perfectly readable” antonyms: illegible, indecipherable 2. easy or enjoyable to read. “a marvellously readable book”
first second
return type Stream<Integer> Stream<PropertyId>
name searchForProperties
buying or renting? boolean renting PurchasingType
monthly budget int monthlyBudget Budget
centre coordinates
double latitude,double longitude Coordinate
maximum distance
double distanceFromCentre Radius
Readability
Stream<Integer> searchResults = searchForProperties( true, 500, 51.525094, -0.127305, 2);
Stream<PropertyId> searchResults = searchForProperties( PropertyType.RENTAL, MonthlyBudget.of(500, GBP), Coordinate.of(51.525094, -0.127305), Radius.of(2, MILES));
Readability
Stream<PropertyId> searchResults = searchForProperties( PropertyType.RENTAL, MonthlyBudget.of(500, GBP), Coordinate.of(51.525094, -0.127305), Radius.of(2, MILES));
Stream<PropertyId> searchResults = searchForProperties( PropertyType.RENTAL, MonthlyBudget.of(500, GBP), CircularArea.around(Coordinate.of( 51.525094, -0.127305)) .with(Radius.of(2, MILES)));
Readability
Stream<PropertyId> searchForProperties( PurchasingType purchasingType, Budget budget, Area area) { ... }
SearchQuery<PropertyId> searchForProperties( PurchasingType purchasingType, Budget budget, Area area) { ... }
Readability
public interface SearchQuery<T> { public Stream<T> fetch(); public T fetchFirst();}
Readability
Readability
OK, the inputs make sense. But the output?
Why bother with a PropertyId?
public final class PropertyId { private final int value; public PropertyId(int value) { this.value = value; } public SearchQuery<Property> query( DatabaseConnection connection) { ... } public void renderTo(Somewhere else) { ... } // equals, hashCode and toString}
Readability
ReadabilityWhat does PropertyId do?
SearchQuery<Property> query (DatabaseConnection connection)
void renderTo(Somewhere else)
new PropertyId(int value)
String toString()
boolean equals(Object other) int hashCode()
Readabilityaddition
multiplication
subtraction
division
modulus
negation
bit manipulation operations such as &, |, ^ and ~
further bit manipulation functionality from java.lang.Integer (I count 9 separate methods)
equality
hashing
comparison with other integers
treatment as an unsigned integer
treatment as a sign (the Integer.signum function returns a value representing negative, positive or zero)
conversion to and from other number types (such as double)
conversion to and from strings in decimal, hexadecimal, octal and binary
all of the other methods on java.lang.Integer
What does int do?
Flexibility
public SearchQuery<PropertyId> searchForProperties( PurchasingType purchasingType, Budget budget, Area area) { Area rectangle = area.asRectangle(); return connection .query(query -> query .select() .from(PROPERTY) .where(PROPERTY.PURCHASING_TYPE.equal( purchasingType.name())) .and(PROPERTY.BUDGET.lessOrEqual(budget.inPounds())) .and(PROPERTY.LONGITUDE .between(rectangle.minX()).and(rectangle.maxX())) .and(PROPERTY.LATITUDE .between(rectangle.minY()).and(rectangle.maxY()))) .filter(row -> area.contains(row.getValue(PROPERTY.LATITUDE), row.getValue(PROPERTY.LONGITUDE))) .map(row -> new PropertyId(row.getValue(PROPERTY.ID)));}
Flexibility
public final class PropertyId { private final int value; ...}
Flexibility
Readability
But our website was slow, so we decided to switch to Cassandra.
public final class PropertyId { private final UUID value; private final int humanRepresentation; ...}
Flexibility
public SearchQuery<PropertyId> searchForProperties( PurchasingType purchasingType, Budget budget, Area area) { Area rectangle = area.asRectangle(); return connection .query(query -> query .select() .from(PROPERTY) .where(PROPERTY.PURCHASING_TYPE.equal( purchasingType.name())) .and(PROPERTY.BUDGET.lessOrEqual(budget.inPounds())) .and(PROPERTY.LONGITUDE .between(rectangle.minX()).and(rectangle.maxX())) .and(PROPERTY.LATITUDE .between(rectangle.minY()).and(rectangle.maxY()))) .filter(row -> area.contains(row.getValue(PROPERTY.LATITUDE), row.getValue(PROPERTY.LONGITUDE))) .map(row -> new PropertyId( row.getValue(PROPERTY.ID), row.getValue(PROPERTY.HUMAN_REPRESENTATION)));}
Flexibility
Correctness
Correctness
In his book, Understanding the Four Rules of Simple Design, Corey Haines talks about a really important concept in software design.
He calls things that embody this concept, behaviour attractors.
public Stream<Integer> searchForProperties( boolean renting, int monthlyBudget, double latitude, double longitude, double distanceFromCentre) { check(distanceFromCentre, is(greaterThan(0))); ...}
Correctness
public final class Radius { public static Radius of( @NotNull double magnitude, @NotNull DistanceUnit unit) { check(magnitude, is(greaterThan(0))); return new Radius(magnitude, unit); } private Radius( @NotNull double magnitude, @NotNull DistanceUnit unit) { ... } ...}
Correctness
Correctness
I haven’t been totally honest with you.
There’s more code than you thought.
Except it’s not really code.
/** * Searches for properties in the database * matching the specified parameters. * * @param renting True if renting, false if buying. * ... * @return A stream of property IDs. * @throws DatabaseQueryException if there is a connection error. */public Stream<Integer> searchForProperties( boolean renting, int monthlyBudget, double latitude, double longitude, double distanceFromCentre) { check(distanceFromCentre, is(greaterThan(0))); ...}
Correctness
/** * Searches for properties in the database * matching the specified parameters. * * @throws DatabaseQueryException * if there is a connection error. */Stream<PropertyId> searchForProperties( PurchasingType purchasingType, Budget budget, Area area) { ... }
Correctness
public interface SearchQuery<T> { /** * @throws DatabaseQueryException * if there is a connection error. */ public Stream<T> fetch(); /** * @throws DatabaseQueryException * if there is a connection error. */ public T fetchFirst();}
Correctness
public interface SearchQuery<T> { public Stream<T> fetch() throws DatabaseQueryException; public T fetchOne() throws DatabaseQueryException;}
Correctness
@Path("/properties")public final class PropertiesResource { private final Template PropertyTemplate = Template.inClassPath("/com/buymoarflats/website/property-details.html"); @GET @Path("/{propertyId}") public Response propertyDetails(@PathParam("propertyId") int id) { try { return propertyResponse(new PropertyId(id)); } catch (DatabaseQueryException e) { return Response.serverError().entity(e).build(); } } private Response propertyResponse(PropertyId id) throws DatabaseQueryException { Output output = formattedProperty(id); if (output == null) { return Response.notFound().entity(id).build(); } return Response.ok(output).build(); } private Output formattedProperty(PropertyId id) throws DatabaseQueryException { Property property = retrieveProperty(id); if (property == null) { return null; } return PropertyTemplate.format(property); } private Property retrieveProperty(PropertyId id) throws DatabaseQueryException { return id.query(connection).fetchOne(); }}
Correctness
public Response propertyDetails(@PathParam("propertyId") int id) { try { return propertyResponse(new PropertyId(id)); } catch (DatabaseQueryException e) { return Response.serverError().entity(e).build(); }}private Response propertyResponse(PropertyId id) throws DatabaseQueryException { Output output = formattedProperty(id); ... }private Output formattedProperty(PropertyId id) throws DatabaseQueryException { Property property = retrieveProperty(id); ... }private Property retrieveProperty(PropertyId id) throws DatabaseQueryException { return id.query(connection).fetchOne();}
Correctness
Correctness
To paraphrase Nat Pryce and Steve Freeman:
Your types are trying to tell you something.
You should listen to them.
public Response propertyDetails(@PathParam("propertyId") PropertyId id) { try { return propertyResponse( id, formattedProperty(retrieveProperty(id))); } catch (DatabaseQueryException e) { return Response.serverError().entity(e).build(); }}private Response propertyResponse(PropertyId id, Output output) { ...}private Output formattedProperty(Property property) { ...}private Property retrieveProperty(PropertyId id) throws DatabaseQueryException { return id.query(connection).fetchOne();}
Correctness
Correctness
Now, what about all that duplication?
Output process(Input thing) { if (thing == null) { return null; } // process thing and return}
Correctness
Output process(Input thing) { if (thing == null) { return null; } else { // process thing and return }}
Correctness
Output process(Input thing) { Function<Input, Output> processor = value -> { // process value and return }; if (thing == null) { return null; } else { return processor.apply(thing); }}
Correctness
Output processOptional( Function<Input, Output> processor, Input thing) { if (thing == null) { return null; } else { return processor.apply(thing); }}
Correctness
Optional<Output> process( Optional<Input> thing) { return thing.map(value -> { // process the thing and return });}
Correctness
public Response propertyDetails(@PathParam("propertyId") PropertyId id) { try { return propertyResponse( id, retrieveProperty(id).map(this::formattedProperty)); } catch (DatabaseQueryException e) { return Response.serverError().entity(e).build(); }}private Response propertyResponse(PropertyId id, Optional<Output> maybeOutput) { return maybeOutput .map(output -> Response.ok(output)) .orElse(Response.notFound().entity(id)) .build(); }private Output formattedProperty(Property property) { return PropertyTemplate.format(property); }
private Optional<Property> retrieveProperty(PropertyId id) throws DatabaseQueryException { return id.query(connection).fetchOne();}
Correctness
@GET@Path("/{propertyId}")public Response propertyDetails( @PathParam("propertyId") PropertyId id) { try { return id.query(connection).fetchOne() .map(PropertyTemplate::format) .map(Response::ok) .orElse(Response.notFound().entity(id)) .build(); } catch (DatabaseQueryException e) { return Response.serverError() .entity(e).build(); }}
Correctness
Correctness
That works with my DatabaseQueryException.
But what if my templating library can throw a TemplateFormattingException too?
public final class Optional<T> { ... public <U> Optional<U> map( Function<T, U> mapper) { ... }}
@FunctionalInterfacepublic interface Function<T, R> { R apply(T t); // does not throw ...}
Correctness
@GET@Path("/{propertyId}")public Response propertyDetails( @PathParam("propertyId") PropertyId id) { try { return id.query(connection).fetchOne() .map(PropertyTemplate::format) .map(Response::ok) .orElse(Response.notFound().entity(id)) .build(); } catch (DatabaseQueryException e) { return Response.serverError() .entity(e).build(); }}
Correctness
enum RequestFailure { ResourceNotFound, DatabaseQueryFailure, TemplateFormattingFailure}
Correctness
abstract class RequestException extends Exception { ... }final class ResourceNotFoundException extends RequestException { ... }final class DatabaseQueryException extends RequestException { ... }final class TemplateFormattingException extends RequestException { ... }
Correctness
Correctness
OK, I’ve got a type that can handle all of my failures.
But what about success?
Well, we can either have success or failure.
And if we fail, we don’t want to go any further.
public interface Either<F, S> { public <T> Either<F, T> then( Function<S, Either<F, T>> function);}
public class Failure<F, S> implements Either<F, S> { public <T> Either<F, T> then(Func<> function) { return new Failure<>(value); }}
public class Success<F, S> implements Either<F, S> { public <T> Either<F, T> then(Func<> function) { return function.apply(value); }}
Correctness
@Path("/properties")public final class PropertiesResource { @GET @Path("/{propertyId}") public Response propertyDetails( @PathParam("propertyId") PropertyId id) { Either<RequestException, Property> property = id.query(connection).fetchOne(); Either<RequestException, Output> output = property.then(PropertyTemplate::format); output .map(Response::ok) .on(ResourceNotFoundException.class, e -> Response.notFound().entity(id)) .failWith(e -> Response.serverError().entity(e)) .build(); }}
Correctness
@Path("/properties")public final class PropertiesResource { @GET @Path("/{propertyId}") public Response propertyDetails( @PathParam("propertyId") PropertyId id) { id.query(connection).fetchOne() .then(PropertyTemplate::format) .map(Response::ok) .on(ResourceNotFoundException.class, e -> notFound().entity(id)) .failWith(e -> serverError().entity(e)) .build(); }}
Correctness
Performance
Performance
On BuyMoarFlats.com, we let you short list properties.
Set<ShortListedProperty> shortList = connection .query(query -> query .select() .from(SHORT_LIST) .join(PROPERTY) .on(SHORT_LIST.PROPERTY_ID .eq(PROPERTY.ID)) .where(SHORT_LIST.USER_ID .eq(user.id()))) .map(row -> propertyFrom(row)) .fetch() .collect(toSet());
Performance
List<ShortListedProperty> sortedShortList = shortList.stream() .sorted(comparing(dateTimeAdded)) .collect(toList());
Performance
Map<City, List<ShortListedProperty>> shortListsByCity = sortedShortList.stream() .collect(groupingBy(city));
Performance
Set<City> cities = shortListByCity.keySet();
Performance
List<ShortListedProperty> upForAuctionSoon =shortListsByCity.values().stream() .flatMap(Collection::stream) .filter(property -> property.isUpForAuctionInLessThan( 1, WEEK)) .collect(toList());
Performance
Stream<Property> randomPromotedAuction = connection .query(query -> query .select() .from(PROPERTY) .where(PROPERTY.SALE_TYPE .eq(PropertySaleType.AUCTION)) .and(PROPERTY.PROMOTED.eq(true)) .limit(1)) .fetch();List<Property> highlighted = Stream.concat(randomPromotedAuction, upForAuctionSoon.stream()) .collect(toList());
Performance
Performance
And we’ve got more features coming every week!
public final class ShortList { Set<ShortListedProperty> shortList; List<ShortListedProperty> sortedShortList; Map<City, List<ShortListedProperty>> byCity; Set<City> cities; List<ShortListedProperty> upForAuctionSoon; Optional<Property> randomPromotedAuction; List<Property> highlighted; ...}
Performance
public final class ShortList { Set<ShortListedProperty> shortList; List<ShortListedProperty> sortedShortList; Map<City, List<ShortListedProperty>> byCity; Set<City> cities; List<ShortListedProperty> upForAuctionSoon; Optional<Property> randomPromotedAuction; List<Property> highlighted; ...}
Performance
Performance
But we process the list five times.
public final class ShortList { Set<ShortListedProperty> shortList; List<ShortListedProperty> sortedShortList; Map<City, List<ShortListedProperty>> byCity; Set<City> cities; List<ShortListedProperty> upForAuctionSoon; Optional<Property> randomPromotedAuction; List<Property> highlighted; ...}
Performance
Performance
Here’s the magic bullet.
We don’t need to optimise our algorithms.
The database author already did that.
We just need to write code that does less.
public final class ShortList { Map<City, List<ShortListedProperty>> byCity; Set<City> cities; List<Property> highlighted; ...}
Performance
In Conclusion
We tackled four discrete problems, but the solution was always the same.
Make a new type.
public class CircularArea implements Area { ...}
public final class Optional<T> { ...}
public final class Either<F, S> { ...}
Quoting @jbrains (who was paraphrasing Kent Beck),
“I define simple design this way.
A design is simple to the extent that it:
1. Passes its tests
2. Minimizes duplication
3. Maximizes clarity
4. Has fewer elements”
http://www.jbrains.ca/permalink/the-four-elements-of-simple-design
1. Passes its tests
2. Minimizes duplication
3. Maximizes clarity
4. Has fewer elements
Jackpot.
Go wrap some data in types.
talks.samirtalwar.com
Heckling starts now.
17th April 2015@SamirTalwar