Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Suggestion: Method Level @FeatGate #565

Open
hbrls opened this issue Dec 28, 2023 · 1 comment
Open

Feature Suggestion: Method Level @FeatGate #565

hbrls opened this issue Dec 28, 2023 · 1 comment

Comments

@hbrls
Copy link

hbrls commented Dec 28, 2023

Is your feature request related to a problem? Please describe.

The webapp renders user's firstname and lastname.

The 1st sprint delivers a simple solution rendering plain values from database.

The 2nd sprint requires to format the values. albert -> Albert, einstein -> E..

The format utils comes from another team. We need a @FeatGate to switch between new format and old no-format, in case the format utils not work.

@FeatGate example from current docs

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/getName")
    @FeatGate("getName", others = {
            @RouteMapping(value = "v1", path = "/getNameV1"),
            @RouteMapping(value = "v2", path = "/getNameV2"),
    })
    public Entity getName() {
        return this.getNameV1();
    }

    @GetMapping("/getNameV1")
    public Entity getNameV1() {
        return { firstName: userService.getFirstNameV1(), lastName: userService.getLastNameV1() };
    }

    @GetMapping("/getNameV2")
    public Entity getNameV2() {
        return { firstName: userService.getFirstNameV2(), lastName: userService.getLastNameV2() };    }
    }
}

The example is a Controller level switch. But we want multiple fine grained flags to switch each independently. Something like:

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/getName")
    public Entity getName() {
        return {
            firstName: switch @FeatGate("getFirstName"):
                           case "v1": userService.getFirstNameV1();
                           case "v2": userService.getFirstNameV2();
                           case "v3": userService.getFirstNameV3();
            lastName: switch @FeatGate("getLastName"):
                          case "v1": userService.getLastNameV1();
                          case "v2": userService.getLastNameV2();
                          case "v3": userService.getLastNameV3();
        };
    }
}

The Feature Sugguestion

We should decouple the @FeatGate flags from code. The impl of UserService should wraps flags and promise to return final values to Controller.

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/getName")
    public Entity getName() {
        return {
            firstName: userService.getFirstName();
            lastName: userService.getLastName();
        };
    }
}

@Service
public class UserServiceImpl implements UserService {

    @FeatGate(flag = "getFirstName", default = "v1")
    public String getFirstName();

    private String getFirstNameV1() {
        return "albert";
    }

    private String getFirstNameV2() {
        return Utils.upperCase("albert"); 
    }

    @FeatGate(flag = "getLastName", default = "v1")
    public String getLastName();

    private String getLastNameV1() {
        return "einstein";
    }

    private String getLastNameV2() {
        return Utils.initial("einstein"); 
    }
}

Feature Suggestion: The Real RFC

We want the variants of impl to share the same name as the interface.

  1. getFirstName, not getFirstNameV1; no v1, v2
  2. the same method name and sig repeats multiple times; need a much more powerful @override
public interface UserService {
    String getFirstName();
    String getLastName();
}

@Service
public class UserServiceImpl implements UserService {

    @FeatGate(flag = "getFirstName", value = "v1")
    @FeatGate(flag = "getFirstName", value = "default")
    private String getFirstName() {
        return "albert";
    }

    @FeatGate(flag = "getFirstName", value = "v2")
    private String getFirstName() {
        return Utils.upperCase("albert"); 
    }

    @FeatGate(flag = "getLastName", default = "v1")
    @FeatGate(flag = "getLastName", default = "default")
    private String getLastName() {
        return "einstein";
    }

    @FeatGate(flag = "getLastName", default = "v2")
    private String getLastName() {
        return Utils.initial("einstein"); 
    }
}

The benefits:

  1. good to read
  2. once the services are stable (online for more than 6 month), we can just delete the deprecated lines, and there is no need to modify the names of getFirstNameV2 back to getFirstName;
  3. a simple test case: there are only - in git diff, no +, no +-
@Service
public class UserServiceImpl implements UserService {

-    @FeatGate(flag = "getFirstName", value = "v1")
-    @FeatGate(flag = "getFirstName", value = "default")
-    private String getFirstName() {
-        return "albert";
-    }

-    @FeatGate(flag = "getFirstName", value = "v2")
    private String getFirstName() {
        return Utils.upperCase("albert"); 
    }
}

Additional context

I guess dynamic languages like Python / JavaScript have dark magics as @decorator to implement that.

But I'd like to see a more standard and no magic implement.

I suggest the annotation name to be @DeliverGate to imply it's about the dev side of code and deliver. Dev can and should delete the lines after stable deliver and without need to notify business departments.

Is this feature something you're interested in working on?

@hbrls
Copy link
Author

hbrls commented Dec 28, 2023

A runnable example to demo the idea better:

public interface UserService {

    String SWITCH_PREFIX = "DeliverGate.";
    String CLASS_PREFIX = "com.example.service.UserService.";

    default String getFirstName() {
        String func = "getFirstName";
        String variant = "v2";
        String flag = ffClient.getVariant(SWITCH_PREFIX + CLASS_PREFIX + func, variant);
        String declared = func + "_" + flag;

        try {
            Method method = this.getClass().getDeclaredMethod(declared);
            method.setAccessible(true);
            return (String) method.invoke(this);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
}

@Service
public class UserServiceImpl implements UserService {

    private String getFirstName_v1() {
        return "albert";
    }

    private String getFirstName_v2() {
        return Utils.upperFirst("albert");
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Feedback
Development

No branches or pull requests

2 participants