Skip to content

Instantly share code, notes, and snippets.

@btforsythe
Last active January 12, 2019 18:41
Show Gist options
  • Save btforsythe/b60f153f43ff4a052ef9ffb53c3d3b15 to your computer and use it in GitHub Desktop.
Save btforsythe/b60f153f43ff4a052ef9ffb53c3d3b15 to your computer and use it in GitHub Desktop.
Feeding and water virtual pets: from names classed to method references
package virtualpet;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class VirtualPetShelter {
private Map<String, VirtualPet> pets = new HashMap<>();
/**
* Feed with for loop and cast. Using this approach, we see similar code
* duplicated throughout the class for water, play, etc. Testing becomes
* onerous, especially if one is properly test driving.
*/
public void feedWithFor() {
for (VirtualPet p : pets.values()) {
if (p instanceof Organic) {
((Organic) p).feed();
}
}
}
/**
* Here we see the duplication in the water method.
*/
public void waterWithFor() {
for (VirtualPet p : pets.values()) {
if (p instanceof Organic) {
((Organic) p).water();
}
}
}
/**
* The way to go about feeding all organics with objects defined as named
* classes in the old school fashion. An immediate benefit is that once we have
* verified that a function works, we need not verify that the same function
* works for other uses (functional composition). We probably would make these
* objects instance variables, but I'm keeping them here for (what I hope is)
* clarity.
*/
public void feedWithObjectsFromNamedClasses() {
OrganicSelector selectOrganics = new OrganicSelector();
PetToOrganicCaster castToOrganic = new PetToOrganicCaster();
Feeder feeder = new Feeder();
pets.values().stream().filter(selectOrganics).map(castToOrganic).forEach(feeder);
}
private class OrganicSelector implements Predicate<VirtualPet> {
@Override
public boolean test(VirtualPet p) {
return p instanceof Organic;
}
}
// we could also do this with a cast inside Feeder, but this is cleaner
private class PetToOrganicCaster implements Function<VirtualPet, Organic> {
@Override
public Organic apply(VirtualPet p) {
return (Organic) p;
}
}
private class Feeder implements Consumer<Organic> {
@Override
public void accept(Organic p) {
p.feed();
}
}
/**
* Here is an obvious refactoring. We can now do various things with organic
* pets by reusing organicPets().
*/
public void feedWithObjectsFromNamedClassesRefactored() {
Feeder feeder = new Feeder();
organicPetsFromNamed().forEach(feeder);
}
private Stream<Organic> organicPetsFromNamed() {
OrganicSelector selectOrganics = new OrganicSelector();
PetToOrganicCaster castToOrganic = new PetToOrganicCaster();
return pets.values().stream().filter(selectOrganics).map(castToOrganic);
}
/**
* Now let's do it with instances of anonymous inner classes. This is common
* when our functions are simple and not reproduced. Creating them as instance
* variables this time, cleaner.
*/
private Predicate<VirtualPet> selectOrganicsAnonymous = new Predicate<VirtualPet>() {
@Override
public boolean test(VirtualPet p) {
return p instanceof Organic;
}
};
private Function<VirtualPet, Organic> castToOrganicAnonymous = new Function<VirtualPet, Organic>() {
@Override
public Organic apply(VirtualPet p) {
return (Organic) p;
}
};
private Consumer<Organic> feederAnonymous = new Consumer<Organic>() {
@Override
public void accept(Organic p) {
p.feed();
}
};
public void feedWithObjectsFromAnonymousClasses() {
pets.values().stream().filter(selectOrganicsAnonymous).map(castToOrganicAnonymous).forEach(feederAnonymous);
}
/**
* Again the obvious refactoring.
*/
public void feedWithObjectsFromAnonymousClassesRefactored() {
organicPetsAnonymous().forEach(feederAnonymous);
}
private Stream<Organic> organicPetsAnonymous() {
return pets.values().stream().filter(selectOrganicsAnonymous).map(castToOrganicAnonymous);
}
/**
* Which allows me to do something like this. All I would need to do for testing
* purposes (assumed I had already test-driven organic pets selection in feed or
* otherwise) would be to check that the watering Consumer works.
*/
public void waterAnonymous() {
organicPetsAnonymous().forEach(new Consumer<Organic>() {
@Override
public void accept(Organic p) {
p.water();
}
});
}
/**
* In Java, we can treat any interface with only one method as a functional
* interface, whether or not they are annotated with
* {@link FunctionalInterface}.
*/
public void feedWithLambdas() {
Predicate<VirtualPet> selectOrganics = p -> p instanceof Organic;
Function<VirtualPet, Organic> castToOrganic = p -> (Organic) p;
Consumer<Organic> feeder = p -> p.feed();
pets.values().stream().filter(selectOrganics).map(castToOrganic).forEach(feeder);
}
/**
* We would often inline these.
*/
public void feedWithLambdasConcise() {
pets.values().stream().filter(p1 -> p1 instanceof Organic).map(p2 -> (Organic) p2).forEach(p -> p.feed());
}
/**
* Again, this refactoring.
*/
public void feedWithLambdasConciseRefactored() {
organicPetsLambda().forEach(p -> p.feed());
}
private Stream<Organic> organicPetsLambda() {
return pets.values().stream().filter(p -> p instanceof Organic).map(p -> (Organic) p);
}
/**
* If we extract a method that allows us to select pets of a given type, we can
* avoid duplication of that concept. To support that, I'll first replace the
* instanceof with {@link Class#isAssignableFrom(Class)} and the cast with a
* method reference to {@link Class#cast(Object)}. (I might have gotten the
* isAssignableFrom backwards.)
*/
private Stream<Organic> organicPetsLambdaRefactored() {
return pets.values().stream().filter(p -> Organic.class.isAssignableFrom(p.getClass()))
.map(Organic.class::cast);
}
/**
* Now I can extract a method, passing it Organic.class. We are creating a
* closure when we do this.
*/
private Stream<Organic> organicPets() {
return petsThatAre(Organic.class);
}
private <T> Stream<T> petsThatAre(Class<T> type) {
return pets.values().stream().filter(p -> type.isAssignableFrom(p.getClass())).map(type::cast);
}
/**
* Final versions. I'm using method references rather than lambda expressions
* because they seem more expressive / easier to parse in this case.
*/
public void feed() {
organicPets().forEach(Organic::feed);
}
public void water() {
organicPets().forEach(Organic::water);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment