Premesso che la leggibilità è sempre un soggetto altamente personale, ciò che per me può essere perfettamente leggibile può non esserlo per altri, la mia opinione è che, dovendo scegliere, è sempre meglio preferire la leggibilità esterna di una classe, piuttosto che quella interna.
Difatti, in teoria (specie se si vuole rispettare OCP), una classe dovrebbe essere modificata il meno possibile, e se scriviamo del buon design ben orientato agli oggetti è probabile che la classe stessa non sarà modificata mai, ma verranno solo aggiunte nuove implementazioni di interfacce a fronte di nuove funzionalità.
"Meglio fuori che dentro, dico sempre io"
La leggibilità interna ha quindi un valore abbastanza relativo, basta cioè che sia sufficientemente leggibile da rispettare principi come avere metodi corti e ben definiti, pochi (meglio niente) cicli for e if statement innestati in cascata (ma piuttosto estratti in metodi), nessuna ambiguità su ingressi e uscite, meglio ancora se con un forte controllo sui tipi (per quanto in java i generics siano molto verbosi, preferisco usarli sempre e comunque quando servono, perchè offrono forti garanzie sulla correttezza dei tipi in uso).
Un esempio concreto: la classe sottostante effettivamente ha una leggibilità interna sufficiente, ma non esaltante.
Le classi inner e l'uso pesante di reflection (purtroppo inevitabile, dato lo scopo di questa classe) sicuramente confondono molto, e anche la gestione delle eccezioni ci mette il suo zampino.
E' anche vero però che la classe principale ha un costruttore privato, e l'unico modo di istanziarla è usando l'unico metodo pubblico disponibile: il metodo "search(enumClass)". Da lì, navigando il codice seguendo lo stack delle chiamate, è possibile seguire il flusso senza perdersi troppo, dato che le biforcazioni non sono molte.
Inoltre i metodi (così come le inner class) sono abbastanza compatti e focalizzati.
Si riesce a capire cosa fa questa classe? (in basso la risposta...)
package com.gmail.gulino.marco.utils; import static com.google.common.collect.Iterables.find; import static com.google.common.collect.Lists.newArrayList; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; import com.google.common.base.Predicate; @SuppressWarnings("unchecked") public class SmartEnums<EnumType extends Enum<EnumType>> { public static <EnumType extends Enum<EnumType>> SmartEnums<EnumType> search(Class<EnumType> enumClass) { return new SmartEnums<EnumType>(enumClass); } private List<EnumType> allValues; private SmartEnums(Class<EnumType> enumClass) { try { Method valuesMethod = enumClass.getMethod("values"); this.allValues = newArrayList((EnumType[]) valuesMethod.invoke(enumClass)); } catch (Exception e) { throw new RuntimeException("Are you trying to apply SmartEnums to a non enum class?"); } } public MemberSearch byField(final String fieldName) { return new MemberSearch() { @Override protected Object findValueOn(EnumType enumType) throws Exception { Field field = enumType.getClass().getDeclaredField(fieldName); field.setAccessible(true); return field.get(enumType); } }; } public MemberSearch byMethod(final String methodName) { return new MemberSearch() { protected Object findValueOn(EnumType enumType) throws Exception { Method method = enumType.getClass().getDeclaredMethod(methodName); method.setAccessible(true); return method.invoke(enumType); } }; } public abstract class MemberSearch { public EnumType havingValue(Object value) { try { return find(allValues, valueFinderPredicate(value)); } catch (Exception e) { throw new RuntimeException(e); } } protected abstract Object findValueOn(EnumType enumType) throws Exception; protected Predicate<EnumType> valueFinderPredicate(final Object value) { return new Predicate<EnumType>() { @Override public boolean apply(EnumType enumType) { try { return value.equals(findValueOn(enumType)); } catch (Exception e) { throw new RuntimeException(e); } } }; } } }
Certo, come ho già scritto prima la leggibilità interna non è esaltante...
Ma vediamo come viene utilizzata esternamente questa classe, nella fattispecie il suo unit test::
package com.gmail.gulino.marco.utils; import static com.gmail.gulino.marco.utils.SmartEnums.search; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; import org.junit.Test; public class SmartEnumsTest { @Test public void itShouldRetrieveEnumByField() throws Exception { assertThat(search(AnExampleEnum.class).byField("enumFieldToSearchFor").havingValue("first"), equalTo(AnExampleEnum.firstElement)); assertThat(search(AnExampleEnum.class).byField("enumFieldToSearchFor").havingValue("second"), equalTo(AnExampleEnum.secondElement)); } @Test public void itShouldRetrieveEnumByMethods() throws Exception { assertThat(search(AnExampleEnum.class).byMethod("enumMethodToSearchFor").havingValue("first"), equalTo(AnExampleEnum.firstElement)); assertThat(search(AnExampleEnum.class).byMethod("enumMethodToSearchFor").havingValue("second"), equalTo(AnExampleEnum.secondElement)); } public enum AnExampleEnum { firstElement("first"), secondElement("second"); private final String enumFieldToSearchFor; private AnExampleEnum(String value) { this.enumFieldToSearchFor = value; } public String enumMethodToSearchFor() { return enumFieldToSearchFor; } } }
Le assert sono chiare: traducendo letteralmente dall'inglese, abbiamo, mettendo tra parentesi le parole sottointese:
Asserisci (che la) ricerca (di) AnExampleEnum per field "enumFieldToSearchFor" avente valore "first" (sia) uguale a AnExampleEnum.firstElementLe parole sottointese sono ben poche, è quindi evidente che fa la classe:
Ricerca una enum per field "nome del field" avente valore "valore da ricercare".o, nella seconda delle due interfacce disponibili:
Ricerca una enum per metodo "nome del metodo" avente valore "valore da ricercare".
Ora, il vantaggio evidente è che gli utilizzatori di questa classe non dovranno scervellarsi troppo nel capirla, in effetti non avranno proprio alcun bisogno di aprire l'implementazione e leggerla. Basta solo leggere il suo test (o un eventuale altro utilizzo) per capire cosa fa e come va invocata.
Sono parzialmente d'accordo. Bisogna anche capire chi sono i lettori di quella classe. Ad esempio pur usando la libreria di apache commons non mi sono mai preoccupato di vedere com'è implementata, mi basta che funzioni a dovere. L'importante è che chi la deve manutenere sappia, in tempi successivi, rimetterci mano senza far fatica.
ReplyDeleteA margine, io mi perdo quando leggo enum < A extends B > extends C
Eh ma direi che è proprio questo il punto.
ReplyDeleteCome puoi sapere, a priori, chi saranno i lettori di quella classe?
Allora, vogliamo scrivere qualcosa che sia "leggibile per chiunque, indipendentemente da chi sia"? Ma non esiste qualcosa del genere... indipendentemente da quale livello di skill scegli, ci sarà sempre qualcuno messo peggio. E ogni volta che abbassi il livello ti precludi alcune "magie" che possono migliorare la leggibilità esterna.
Ad esempio, sui generics di java posso essere d'accordo, sono proprio brutti. MA ti costringono al type-safe e ti evitano di fare cast: insomma, se peggiorano un po' (ma se ci stai attento, neanche tanto) la leggibilità interna, migliorano non poco quella esterna.