Strategy Pattern con Spring Boot

Un buen patrón para resolver la complejidad cuando debes implementar diferentes comportamientos según algún estado es el patrón strategy.

Un patrón strategy encapsula comportamiento que podemos intercambiar en ejecución. Es decir, cambiar el comportamiento o lógica de negocio acorde al estado del modelo o contexto en el que te se encuentra, evitando el uso extensivo de if o switch en tu código.

Es bastante simple. Necesitas una interfaz Strategy y diferentes implementaciones para la resolución de los diferentes comportamientos deseados.

Para este ejemplo suponemos que tenemos una entidad “User” que representa un usuario. Estos users son clasificados por tipo acorde a un enum “UserType”. El UserType determinará el valor del atributo limitCredit del User.

@Entity
public class User {

   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;

   @NotBlank
   private String userName;

   @NotBlank
   private String lastName;

   private UserType type;

   private double limitCredit;

}

public enum UserType {
   NORMAL, FULL, GOLD;
}

Queremos que nuestro servicio se desentienda de esta decisión, por lo que crearemos diferentes estrategias para cada tipo de user.

Cómo crear el patrón Strategy

Defines la interfaz de la cual luego vas a implementar los diferentes comportamientos. En este ejemplo simple se trata de cambiar el limite bajo algún cálculo que suponemos complejo para los diferentes tipos de User.

public interface Strategy {
   void changeLimit(User user);
}

Las implementaciones de las estrategias para cada tipo UserType quedan así:

public class StrategyOperationNormal implements Strategy {
   @Override
   public void changeLimit(User user) {
       // a complex calculation.. 
       user.setType(UserType.NORMAL);
       user.setLimitCredit(1000D);
   }
}

public class StrategyOperationFull implements Strategy {
   @Override
   public void changeLimit(User user) {
       user.setType(UserType.FULL);
       user.setLimitCredit(5000D);
   }
}
public class StrategyOperationGold implements Strategy {

   @Override
   public void changeLimit(User user) {
       user.setType(UserType.GOLD);
       user.setLimitCredit(20000D);
   }

}

Usar un Factory que nos devuelva el Strategy

Para simplificar aún más el uso de Strategy en tu servicio creas una clase StrategyFactory. Este factory te devolverá la estrategia correspondiente según el UserType.

Un patrón factory te permite crear objetos bajo alguna condición sin necesidad de conocer o preocuparte acerca de la implementación real, en definitiva solo conoces la interfaz.

@Component
public class StrategyFactory {

   private Map<UserType, Strategy> strategies = new EnumMap<>(UserType.class);

   public StrategyFactory() {
       initStrategies();
   }

   public Strategy getStrategy(UserType userType) {
       if (userType == null || !strategies.containsKey(userType)) {
           throw new IllegalArgumentException("Invalid " + userType);
       }
       return strategies.get(userType);
   }

   private void initStrategies() {
       strategies.put(UserType.NORMAL, new StrategyOperationNormal());
       strategies.put(UserType.FULL, new StrategyOperationFull());
       strategies.put(UserType.GOLD, new StrategyOperationGold());
   }

}

En tu servicio haces uso del factory para obtener la estrategia y delegarle la lógica de cambio de tipo de UserType en el metodo “changeType” .

@Service
public class UserService {

   private final StrategyFactory strategyFactory;
   private final UserRepository repository;

   @Autowired
   public UserService(UserRepository repository, StrategyFactory strategyFactory) {
       this.repository = repository;
       this.strategyFactory = strategyFactory;
   }

   // other methods of service..

   public User changeType(long id, UserType type) {
       Strategy strategy = strategyFactory.getStrategy(type);
       User user = repository.findOne(id);
       strategy.changeLimit(user);
       return repository.save(user);
   }

}

Probando tu Strategy

Usando el Controller de la app, que tenemos ya definido, creamos un User. Por defecto el User se crea con un tipo “NORMAL”.

El controller de nuestra app

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/users", method = RequestMethod.GET)
    public ResponseEntity<User> list() {
        List<User> users = userService.list();
        return new ResponseEntity(users, HttpStatus.OK);
    }

    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public ResponseEntity<User> userById(@RequestParam(value = "id") long id) {
        User user = userService.get(id);
        return new ResponseEntity(user, HttpStatus.OK);
    }

    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public ResponseEntity<User> create(@Valid @RequestBody User user) {
        User userCreated = userService.create(user);
        return new ResponseEntity(userCreated, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/change_type", method = RequestMethod.POST)
    public ResponseEntity<User> changeType(@RequestParam(value = "id") long id,
                                           @RequestParam(value = "type") UserType type) {
        User userCreated = userService.changeType(id, type);
        return new ResponseEntity(userCreated, HttpStatus.CREATED);
    }

}

El Service completo de nuestra app

@Service
public class UserService {

    private final StrategyFactory strategyFactory;
    private final UserRepository repository;

    @Autowired
    public UserService(UserRepository repository, StrategyFactory strategyFactory) {
        this.repository = repository;
        this.strategyFactory = strategyFactory;
    }

    public User get(long userId) {
        return repository.findOne(userId);
    }

    public List<User> list() {
        Iterable<User> users = repository.findAll();
        List<User> list = new ArrayList<User>();
        users.forEach(list::add);
        return list;
    }

    public User create(User user) {
        Strategy strategy = strategyFactory.getStrategy(UserType.NORMAL);
        strategy.changeLimit(user);
        return repository.save(user);
    }

    public User changeType(long id, UserType type) {
        Strategy strategy = strategyFactory.getStrategy(type);
        User user = repository.findOne(id);
        strategy.changeLimit(user);
        return repository.save(user);
    }

}

Ejecutas el rest api “/create” para crear un User que luego vamos a modificar. Enviamos en el body lo mínimo que necesita la entidad User.

El user se crea con el type “NORMAL” por defecto.

Luego ejecutas el rest api “/change_type?id=1&type=FULL” para probar tu Servicio que usa el Factory y el Strategy. Aquí intentas cambiar el type de NORMAL a FULL para el user previamente creado.

Observa que el user ha cambiado su límite acorde a la Strategy.

Descarga este código completo desde GitHub

Hi! If you find my posts helpful, please support me by inviting me for a coffee :)

Ver también