Muerte a Spring

En el título me refiero al Spring Framework de Java. Pero no se tome al pie de la letra ese título. La idea del post es mostrar que podemos usar mal algo, como una librería, cuando el contexto y los casos de usos no son los adecuados. Muchas veces, como programadores, usamos lo que conocemos, pero no siempre es el camino más adecuado a tomar dado los casos de uso que tenemos entre manos. Es por eso que muchas veces hago énfasis en tener en cuenta los casos de uso y el contexto.

Spring Framework nació en este siglo de la mano de Rod Johnson para poner orden en el desarrollo de un proyecto Java. Una de sus primeras “features” es permitir configurar un grafo de objetos desde un archivo o declaración XML. Eso permitía, hasta en deployment, configurar la conducta de nuestra aplicación, por ejemplo, cambiar el objeto concreto de persistencia, para pasar de usar Oracle a usar otra base de datos. Con el tiempo, esa capacidad de configuración se extendió al uso o no de otros frameworks, por ejemplo, de ORMs como Hibernate.

Pero había que mantener esos archivos XML de configuración. No recuerdo qué framework permitió declarar en atributos la relación entre los objetos, y Spring adoptó con el tiempo esa estrategia también.

Hoy quiero apuntar a código real, de un proyecto de código abierto, que comencé a conocer en @RSKSmart hace un año: la implementación en Java de Ethereum:

https://github.com/ethereum/ethereumj

Es un servidor que ejecuta en una red de servidores similares. implementando una blockchain con smart contracts. Hay implementaciones en otros lenguajes, como Go y Python. Al arrancar, levanta un conjunto de objetos, algunos apuntan a otros, y comienza a funcionar. Pero veamos, como ejemplo, un objeto de los que levanta, el WorldManager:

https://github.com/ethereum/ethereumj/blob/develop/ethereumj-core/src/main/java/org/ethereum/manager/WorldManager.java

Veamos parte del código inicial:

 @Autowired
private PeerClient activePeer;

@Autowired
private ChannelManager channelManager;

@Autowired
private AdminInfo adminInfo;

@Autowired
private NodeManager nodeManager;

@Autowired
private SyncManager syncManager;

@Autowired
private FastSyncManager fastSyncManager;

@Autowired
private SyncPool pool;

@Autowired
private PendingState pendingState;

@Autowired
private UDPListener discoveryUdpListener;

@Autowired
private EventDispatchThread eventDispatchThread;

@Autowired
private DbFlushManager dbFlushManager;
    
@Autowired
private ApplicationContext ctx;

y además usando @Autowired en el constructor:

@Autowired
public WorldManager(final SystemProperties config, final Repository repository,
                    final EthereumListener listener, final Blockchain blockchain,
                    final BlockStore blockStore) {
    this.listener = listener;
    this.blockchain = blockchain;
    this.repository = repository;
    this.blockStore = blockStore;
    this.config = config;
    loadBlockchain();
}

En total, más de una docena de objetos referenciados y creados al principio de la vida de WorldManager. Además, muchos de esos objetos tienen en su implementación más @Autowired. Prácticamente todos estos objetos terminan siendo “singletons”, creados al y relacionados al principio, y nada más. Pero muchos terminan siendo entonces objetos globales, con un estado mutable, que dificulta muchas veces su uso adecuado. En un objeto podemos tener acceso entonces al objeto “global” K, pero cualquier otro código puede alterar el estado de K mientras lo estamos usando. Es notable ese caso en el uso de BlockchainImpl y de Repository, pero eso daría para tema de otro post.

Veamos también la implementación de la interface Ethereum:

https://github.com/ethereum/ethereumj/blob/develop/ethereumj-core/src/main/java/org/ethereum/facade/EthereumImpl.java

Vemos ahí:

@Autowired
WorldManager worldManager;

@Autowired
AdminInfo adminInfo;

@Autowired
ChannelManager channelManager;

@Autowired
ApplicationContext ctx;

@Autowired
BlockLoader blockLoader;

@Autowired
ProgramInvokeFactory programInvokeFactory;

@Autowired
Whisper whisper;

@Autowired
PendingState pendingState;

@Autowired
SyncManager syncManager;

@Autowired
CommonConfig commonConfig = CommonConfig.getDefault();

más objetos referenciados mágicamente con @Autowired. Y de nuevo más en el constructor:

@Autowired
public EthereumImpl(final SystemProperties config, final CompositeEthereumListener compositeEthereumListener) {
    this.compositeEthereumListener = compositeEthereumListener;
    this.config = config;
    System.out.println();
    this.compositeEthereumListener.addListener(gasPriceTracker);
    gLogger.info("EthereumJ node started: enode://" + Hex.toHexString(config.nodeId()) + "@" + config.externalIp() + ":" + config.listenPort());
}


Recordemos que @Autowired es una anotación de Spring que, cuando es el encargado de construir el objeto, completa automáticamente estas referencias anotadas con autowired, con objetos QUE NO SABEMOS cuáles son así viendo el código de arriba, desperdigados por todo el resto del proyecto como beans que cumplen con lo que pide el autowired.

Y notemos que el EthereumImpl, con todos sus objetos referenciados automáticamente, para colmo referencia a un WorldManager. Ambas implementaciones terminan referenciando muchos objetos. Esto tiene toda la facha de “code smell”.

Alguien podría decir: “pero seguramente es necesario, porque el sistema es complejo y tiene muchas relaciones…”. Despues de haber estado un año trabajando sobre este código, usándolo como base, les puedo asegurar que bien se podría implementar mucho más fácil. Lo que sospecho que pasó: el @Autowired es un camino de ida, y cada vez fue más fácil agregar un campo autowired a uno de los objetos ya existentes, que sentarse a pensar cómo armar un grafo de objetos inicial, donde a cada objeto se le inyecte sus colaboradores APROPIADOS. De hecho, es prácticamente el único uso de Spring en el proyecto: armar el grafo inicial. No hay “lifecycle” de objetos ni nada más. Tranquilamente se puede armar un grafo de objetos más armónico, con un código propio. Además, sería más fácil de escribir tests, donde por código, queda explícito que objetos usa cada objeto para funcionar.

He dejado de lado algunos otros temas, como el uso de @PostInit para completar el estado de un objeto, luego de construirlo, y el armado de los listeners: ¿cuándo un objeto comienza a escuchar los eventos de otro? Toda esta conducta queda desperdigada en anotaciones y código separado, y prácticamente sin tests. Y un @Autowired mal resuelto puede detectarse recién con la ejecución del servidor. El proyecto original es muy interesante, y tiene otras cualidades. Pero hoy le tocó el turno a esto que veo como anti patrón, y un claro ejemplo de abandonar lo simple (crear el grafo nosotros) por lo fácil (que lo haga Spring).

En definitiva, el ABUSO de @Autowired ha derivado en la existencia de dos objetos “bolsa de gatos”, objetos “God”. Spring Framework es una gran herramienta, pero como toda herramienta, hay que usarla adecuadamente y respetando el contexto. Espero que en el proyecto en el que estoy trabajando, podamos erradicar esta complejidad y conseguir una implementación más simple. No repitan esto en su casa! 😉

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

This entry was posted in Ethereum, Java, Programacion, Proyectos Open Source, Spring Framework. Bookmark the permalink.