Lambdas, avant multiple avec moulage
besoin d'aide pour penser à lambdas de la part de mes collègues luminaires Stacoverflow.
cas Standard de sélection à travers une liste d'une liste d'une liste pour recueillir quelques enfants profondément dans un graphique. Quels moyens impressionnants pourraient Lambdas
aide avec ce boilerplate?
public List<ContextInfo> list() {
final List<ContextInfo> list = new ArrayList<ContextInfo>();
final StandardServer server = getServer();
for (final Service service : server.findServices()) {
if (service.getContainer() instanceof Engine) {
final Engine engine = (Engine) service.getContainer();
for (final Container possibleHost : engine.findChildren()) {
if (possibleHost instanceof Host) {
final Host host = (Host) possibleHost;
for (final Container possibleContext : host.findChildren()) {
if (possibleContext instanceof Context) {
final Context context = (Context) possibleContext;
// copy to another object -- not the important part
final ContextInfo info = new ContextInfo(context.getPath());
info.setThisPart(context.getThisPart());
info.setNotImportant(context.getNotImportant());
list.add(info);
}
}
}
}
}
}
return list;
}
Remarque: la liste elle-même est d'aller vers le client JSON
, alors ne vous concentrez pas sur ce qui est retourné. Il doit y avoir quelques façons de couper les boucles.
intéressé de voir ce que mes collègues experts créer. Plusieurs approches sont encouragées.
EDIT
findServices
et les deux findChildren
méthodes retourner les tableaux
EDIT-BONUS CHALLENGE
la "partie non importante" s'est avérée être importante. J'ai réellement besoin de copier une valeur uniquement disponible dans le host
instance. Cela semble ruiner tous les beaux exemples. Comment pourrait-on réaliser l'état de l'avant?
final ContextInfo info = new ContextInfo(context.getPath());
info.setHostname(host.getName()); // The Bonus Challenge
4 réponses
il est assez profondément imbriqué mais il ne semble pas exceptionnellement difficile.
la première observation est que si une boucle for se traduit par un flux, les boucles For-nestées peuvent être "aplaties" en un seul flux en utilisant flatMap
. Cette opération prend un seul élément et renvoie un nombre arbitraire d'éléments dans un flux. J'ai regardé et constaté que StandardServer.findServices()
retourne un tableau de Service
alors nous transformons ceci en un flux en utilisant Arrays.stream()
. (Je fais des hypothèses analogues pour Engine.findChildren()
et Host.findChildren()
.
Ensuite, la logique à l'intérieur de chaque boucle n' instanceof
vérifier et d'un casting. Cela peut être modélisé en utilisant des flux comme un filter
opération à réaliser l' instanceof
suivi de map
opération qui jette et renvoie simplement la même référence. Il s'agit en fait d'un no-op mais il permet au système de frappe statique de convertir un Stream<Container>
Stream<Host>
par exemple.
appliquant ces transformations aux boucles imbriquées, on obtient ce qui suit:
public List<ContextInfo> list() {
final List<ContextInfo> list = new ArrayList<ContextInfo>();
final StandardServer server = getServer();
Arrays.stream(server.findServices())
.filter(service -> service.getContainer() instanceof Engine)
.map(service -> (Engine)service.getContainer())
.flatMap(engine -> Arrays.stream(engine.findChildren()))
.filter(possibleHost -> possibleHost instanceof Host)
.map(possibleHost -> (Host)possibleHost)
.flatMap(host -> Arrays.stream(host.findChildren()))
.filter(possibleContext -> possibleContext instanceof Context)
.map(possibleContext -> (Context)possibleContext)
.forEach(context -> {
// copy to another object -- not the important part
final ContextInfo info = new ContextInfo(context.getPath());
info.setThisPart(context.getThisPart());
info.setNotImportant(context.getNotImportant());
list.add(info);
});
return list;
}
Mais attendez, il y a plus.
final forEach
opération est un peu plus compliqué map
opération qui convertit un Context
dans un ContextInfo
. En outre, ils sont simplement recueillis dans un List
donc nous pouvons utiliser des collecteurs pour faire cela au lieu de créer et vider la liste à l'avance et puis la remplir. L'application de ces modifications donne les résultats suivants:
public List<ContextInfo> list() {
final StandardServer server = getServer();
return Arrays.stream(server.findServices())
.filter(service -> service.getContainer() instanceof Engine)
.map(service -> (Engine)service.getContainer())
.flatMap(engine -> Arrays.stream(engine.findChildren()))
.filter(possibleHost -> possibleHost instanceof Host)
.map(possibleHost -> (Host)possibleHost)
.flatMap(host -> Arrays.stream(host.findChildren()))
.filter(possibleContext -> possibleContext instanceof Context)
.map(possibleContext -> (Context)possibleContext)
.map(context -> {
// copy to another object -- not the important part
final ContextInfo info = new ContextInfo(context.getPath());
info.setThisPart(context.getThisPart());
info.setNotImportant(context.getNotImportant());
return info;
})
.collect(Collectors.toList());
}
j'essaie habituellement d'éviter les lambdas multi-lignes (comme dans la finale map
opération) donc j'aurais refactoriser le code dans une petite aide de la méthode qui prend un Context
et renvoie un ContextInfo
. Cela ne raccourcit pas le code du tout, mais je pense que cela le rend plus clair.
UPDATE
mais attendez, il y a encore plus.
extrayons l'appel à service.getContainer()
dans son propre pipeline de l'élément:
return Arrays.stream(server.findServices())
.map(service -> service.getContainer())
.filter(container -> container instanceof Engine)
.map(container -> (Engine)container)
.flatMap(engine -> Arrays.stream(engine.findChildren()))
// ...
ceci expose la répétition du filtrage sur instanceof
suivi d'un mapping avec un plâtre. Faites cela trois fois au total. Il il semble probable que d'autres codes vont avoir besoin de faire des choses similaires, donc il serait bien d'extraire ce peu de logique dans une méthode d'aide. Le problème est que filter
peut changer le nombre d'éléments dans le flux (en laissant tomber ceux qui ne correspondent pas) mais il ne peut pas changer leurs types. Et map
pouvez modifier les types d'éléments, mais il ne peut pas changer de numéro. Quelque chose peut changer à la fois le nombre et les types? Oui, c'est notre vieil ami flatMap
encore une fois! Donc notre méthode d'aide doit prendre un élément et le retour d'un flux d'éléments d'un type différent. Ce flux de retour contiendra un seul élément moulé (s'il correspond) ou il sera vide (s'il ne correspond pas). La fonction helper ressemblerait à ceci:
<T,U> Stream<U> toType(T t, Class<U> clazz) {
if (clazz.isInstance(t)) {
return Stream.of(clazz.cast(t));
} else {
return Stream.empty();
}
}
(C'est vaguement basé sur C#OfType
construction mentionnée dans certains des commentaires.)
tant que nous y sommes, extrayons une méthode pour créer un ContextInfo
:
ContextInfo makeContextInfo(Context context) {
// copy to another object -- not the important part
final ContextInfo info = new ContextInfo(context.getPath());
info.setThisPart(context.getThisPart());
info.setNotImportant(context.getNotImportant());
return info;
}
Après ces extractions, le pipeline ressemble ceci:
return Arrays.stream(server.findServices())
.map(service -> service.getContainer())
.flatMap(container -> toType(container, Engine.class))
.flatMap(engine -> Arrays.stream(engine.findChildren()))
.flatMap(possibleHost -> toType(possibleHost, Host.class))
.flatMap(host -> Arrays.stream(host.findChildren()))
.flatMap(possibleContext -> toType(possibleContext, Context.class))
.map(this::makeContextInfo)
.collect(Collectors.toList());
plus agréable, je pense, et nous avons enlevé la redoutable déclaration à plusieurs lignes lambda.
MISE À JOUR: BONUS CHALLENGE
encore une Fois, flatMap
est votre ami. Prenez la queue du ruisseau et migrez-la dans la dernière flatMap
avant la queue. De cette façon, le host
la variable est toujours en scope, et vous pouvez la passer à un makeContextInfo
méthode helper qui a été modifiée pour prendre host
.
return Arrays.stream(server.findServices())
.map(service -> service.getContainer())
.flatMap(container -> toType(container, Engine.class))
.flatMap(engine -> Arrays.stream(engine.findChildren()))
.flatMap(possibleHost -> toType(possibleHost, Host.class))
.flatMap(host -> Arrays.stream(host.findChildren())
.flatMap(possibleContext -> toType(possibleContext, Context.class))
.map(ctx -> makeContextInfo(ctx, host)))
.collect(Collectors.toList());
ce serait ma version de votre code en utilisant JDK 8 streams, method references et lambda expressions:
server.findServices()
.stream()
.map(Service::getContainer)
.filter(Engine.class::isInstance)
.map(Engine.class::cast)
.flatMap(engine -> Arrays.stream(engine.findChildren()))
.filter(Host.class::isInstance)
.map(Host.class::cast)
.flatMap(host -> Arrays.stream(host.findChildren()))
.filter(Context.class::isInstance)
.map(Context.class::cast)
.map(context -> {
ContextInfo info = new ContextInfo(context.getPath());
info.setThisPart(context.getThisPart());
info.setNotImportant(context.getNotImportant());
return info;
})
.collect(Collectors.toList());
dans cette approche je remplace vos if-statements pour filtrer les prédicats. Prendre en compte qu'un instanceof
vérification peut être remplacé par un Predicate<T>
Predicate<Object> isEngine = someObject -> someObject instanceof Engine;
qui peut aussi être exprimé par
Predicate<Object> isEngine = Engine.class::isInstance
de Même, vos moulages peuvent être remplacés par Function<T,R>
.
Function<Object,Engine> castToEngine = someObject -> (Engine) someObject;
ce qui est à peu près le même
Function<Object,Engine> castToEngine = Engine.class::cast;
et ajouter des éléments manuellement à une liste dans la boucle for peut être remplacé par un collecteur. Dans le code de production, la lambda qui transforme un Context
dans un ContextInfo
peut (et doit) être extraite dans une méthode distincte et utilisée comme référence de méthode.
Solution de bonus challenge
inspiré par la réponse de @EdwinDalorzo.
public List<ContextInfo> list() {
final List<ContextInfo> list = new ArrayList<>();
final StandardServer server = getServer();
return server.findServices()
.stream()
.map(Service::getContainer)
.filter(Engine.class::isInstance)
.map(Engine.class::cast)
.flatMap(engine -> Arrays.stream(engine.findChildren()))
.filter(Host.class::isInstance)
.map(Host.class::cast)
.flatMap(host -> mapContainers(
Arrays.stream(host.findChildren()), host.getName())
)
.collect(Collectors.toList());
}
private static Stream<ContextInfo> mapContainers(Stream<Container> containers,
String hostname) {
return containers
.filter(Context.class::isInstance)
.map(Context.class::cast)
.map(context -> {
ContextInfo info = new ContextInfo(context.getPath());
info.setThisPart(context.getThisPart());
info.setNotImportant(context.getNotImportant());
info.setHostname(hostname); // The Bonus Challenge
return info;
});
}
première tentative au-delà de la laideur. Il me faudra des années avant de trouver ça lisible. Doit y avoir une meilleure façon.
Notez le findChildren
les méthodes renvoient des tableaux qui, bien sûr, fonctionnent avec for (N n: array)
la syntaxe, mais pas avec la nouvelle Iterable.forEach
méthode. Avait les envelopper avec Arrays.asList
public List<ContextInfo> list() {
final List<ContextInfo> list = new ArrayList<ContextInfo>();
final StandardServer server = getServer();
asList(server.findServices()).forEach(service -> {
if (!(service.getContainer() instanceof Engine)) return;
final Engine engine = (Engine) service.getContainer();
instanceOf(Host.class, asList(engine.findChildren())).forEach(host -> {
instanceOf(Context.class, asList(host.findChildren())).forEach(context -> {
// copy to another object -- not the important part
final ContextInfo info = new ContextInfo(context.getPath());
info.setThisPart(context.getThisPart());
info.setNotImportant(context.getNotImportant());
list.add(info);
});
});
});
return list;
}
les méthodes d'utilité
public static <T> Iterable<T> instanceOf(final Class<T> type, final Collection collection) {
final Iterator iterator = collection.iterator();
return () -> new SlambdaIterator<>(() -> {
while (iterator.hasNext()) {
final Object object = iterator.next();
if (object != null && type.isAssignableFrom(object.getClass())) {
return (T) object;
}
}
throw new NoSuchElementException();
});
}
et enfin une implémentation Lambda-powerable de Iterable
public static class SlambdaIterator<T> implements Iterator<T> {
// Ya put your Lambdas in there
public static interface Advancer<T> {
T advance() throws NoSuchElementException;
}
private final Advancer<T> advancer;
private T next;
protected SlambdaIterator(final Advancer<T> advancer) {
this.advancer = advancer;
}
@Override
public boolean hasNext() {
if (next != null) return true;
try {
next = advancer.advance();
return next != null;
} catch (final NoSuchElementException e) {
return false;
}
}
@Override
public T next() {
if (!hasNext()) throw new NoSuchElementException();
final T v = next;
next = null;
return v;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
beaucoup de plomberie et sans doute 5x le code octet. Doit être un meilleure façon de faire.