Skip to content

Add a Reflection proxy utility class #2053

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

Merged
merged 6 commits into from
Nov 16, 2021

Conversation

leocck
Copy link
Contributor

@leocck leocck commented Nov 2, 2021

Stubs with Reflection

While solving the basic concept called "Cook your Lasagna", I noticed the tests would not run until I had implemented all the methods > reason being, the tests were calling the methods from the class directly, causing a build error.

To make the code compile, we should create stubs for the methods the students will implement, but the Lasagna exercise is about how to define methods, and by defining the methods in stubs we spoil the exercise somewhat.

This PR adds a utility class called ReflectionProxy, which simplifies the use of Reflection API by encapsulating the target class. With it, it is easy to have proxied stubs and check if the methods were implemented correctly at the same time.

This utility class was just copied over from source code we use at the ReDI School of Digital Integration with our students and might deserve some improvements. Here's an example of the exercises we are using it.


Reviewer Resources:

Track Policies

The proxy encapsulates the target class and helps checking if the methods were implemented correctly
try {
return invokeMethod("expectedMinutesInOven", new Class[]{});
} catch (Exception e) {
return 0;
Copy link
Contributor Author

@leocck leocck Nov 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you will probably want to throw a UnsupportedOperationException, but this way also works

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen all the implementations of Reflection Proxies throw UnsupportedOperationException as you mentioned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


import static java.lang.Class.forName;

public abstract class ReflectionProxy {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is long because features were added from time to time, in a collaborative but unorganised way.
I made my best to document all the methods before bringing it to this repository, but I'm sure you will find a lot of things that could be improved.

@iHiD
Copy link
Member

iHiD commented Nov 3, 2021

This is a really interesting approach. Thanks. I'll let the Java maintainers review it etc, but just wanted to say thanks for the PR :)

@ericbalawejder
Copy link
Contributor

ericbalawejder commented Nov 3, 2021

I like the idea of testing for the correct method signature without the build failure. This is more of an issue when using the browser editor rather than local tools which was introduced in v3.

As you implemented the Lasagna class for the new tests, the tests pass that check the method stub. Should we create stubs that cause all the tests to fail? Such as omitting the access modifier and changing the return type to void? This would be more consistent with the TDD approach for building each problem.

@leocck
Copy link
Contributor Author

leocck commented Nov 3, 2021

I like the idea of testing for the correct method signature without the build failure. This is more of an issue when using the browser editor rather than local tools which was introduced in v3.

As you implemented the Lasagna class for the new tests, the tests pass that check the method stub. Should we create stubs that cause all the tests to fail? Such as omitting the access modifier and changing the return type to void? This would be more consistent with the TDD approach for building each problem.

I think the idea of this exercise is to provide them an empty class, so that they implement the whole method (signature and logic) by themselves. If we provide them wrong code, then I think the exercise description will have to be changed to "fix the provided code" or something like that. WDYT?

Just to see if I understood you correctly: you mean the Lasagna class they will have to implement (the template), right? The one from the util package is just a proxy (if the methods do not exist in the target class, or have the wrong signature, the tests will still fail).

@ericbalawejder
Copy link
Contributor

Just to see if I understood you correctly: you mean the Lasagna class they will have to implement (the template), right? The one from the util package is just a proxy (if the methods do not exist in the target class, or have the wrong signature, the tests will still fail).

The idea is to provide a template for the student as such?

public class Lasagna extends ReflectionProxy {

    // Implement methods here

    @Override
    public String getTargetClassName() {
        return null;
    }

}

@leocck
Copy link
Contributor Author

leocck commented Nov 3, 2021

The idea is to provide a template for the student as such?

No, that's not the place. The students will get an empty class, from the src/main folder (this file).

The one you are referring to is inside the src/test folder and is used by the tests as a Proxy (it will try to call the one that the students must implement and check if they did it correctly) 😉

I know, it's a bit confusing because both classes have the same name (even though they are in different packages), but that was intended, so when the students look at the unit test code (to understand why it is failing) they could still understand which logic is being tested (without polluting the code with a lot of Reflection calls).

Does it make more sense now?

@ericbalawejder
Copy link
Contributor

Yes, I had the path mixed up as you pointed out. I understand it as unit test --> proxy --> collaborator where the collaborator is the empty file src/main/java/Lasagna?

I like the idea and implementation.

* An instance of the target class (if found)
*/
private Object target;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target can be final for this implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

public ReflectionProxy(Object target) {
this.target = target;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't technically matter for instantiation but would it be more valuable to change the constructors to protected to control scope?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were there two constructors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor that receives the target instance is not being used. Moved to the "unused" region 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the main constructor is still marked public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's ok, since the class is abstract, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't technically matter for instantiation but would it be more valuable to change the constructors to protected to control scope?

It's more of a style thing since this may be used as a template in further exercises.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, done!

package utils;

import java.lang.reflect.*;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;

It helps future maintainers when we avoid wildcard imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return false;
}
try {
Constructor c = targetClass.getDeclaredConstructor(parameterTypes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor<?> c = targetClass.getDeclaredConstructor(parameterTypes);

Wildcard to avoid raw types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (targetClass == null) {
return null;
}
Constructor[] constructors = getAllConstructors();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor<?>[] constructors = getAllConstructors();

Wild card type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return false;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all the methods are used but it seems like a good template for future use in other exercises.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I thought we could leave them for reference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to comment the unused methods with a explicit message like Unused at moment or something like that.
It can improve the time to understand on the code for a maintainer who unknows it

Copy link
Contributor Author

@leocck leocck Nov 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. What do you say about grouping the unused methods with a region section? (it works for almost all editors and is still "understandable" in case it is not supported)

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a good idea to create a Gradle package that contains the reflection proxy, as it can then be re-used across exercises. We've done a similar thing with C#: https://github.com/exercism/dotnet-tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ErikSchierboom : that's exactly what I wanted to do, but I don't know how to setup an internal Gradle package 😅 . If you point me in the right direction I will do it 🙏

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to ask @iHiD to create a repository. Ideas on a good name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, ok, I thought we could move this class to an upper folder and be able to share it with all exercises.

Naming is the most difficult part 😅 . Are there any other utility libraries to refer to, or conventions already?

If not, I would suggest something like exercism:test-utils

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have any conventions. We don't yet support shared files between exercises and it might be nice to hide those files from the student anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, should we merge this PR and create another ticket for the new repository? (are you using Jira or other issue tracking?)

@Test
public void expected_minutes_in_oven() {
assertThat(new Lasagna().expectedMinutesInOven()).isEqualTo(40);
}

@Ignore("Remove to run test")
@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the test runner actually removes the @Ignore annotation when it runs the tests on the Exercism server. The @Ignore annotation is normally placed on every test except the first one for local development purposes, i.e. exercism download --exercise=lasagna --track=java with the command line tool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I didn't know that, but it makes a lot of sense now

@leocck
Copy link
Contributor Author

leocck commented Nov 4, 2021

I like the idea and implementation.

Yay! Thanks for your review, I fixed all the issues you pointed out.

@ErikSchierboom
Copy link
Member

Great idea! I'm planning to do something similar for the C# track.

@ericbalawejder
Copy link
Contributor

@mirkoperillo The build action: Compile check style with gradle fails and has 65K+ lines of output. I assume it's running all of the exercises? Is this normal or intended behavior?

ericbalawejder
ericbalawejder approved these changes Nov 4, 2021
@mirkoperillo
Copy link
Contributor

@ericbalawejder you have right. Currently when the CI workflow starts the gradle check command is executed on all the exercises. We need to improve it

run: cd exercises && ../gradlew check compileStarterSourceJava --parallel --continue --info

Copy link
Contributor

@mirkoperillo mirkoperillo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leocck sorry for the late feedback, your PR is alright: I run the tests against it and work fine.
Currently the code breaks some style constraints.
Can I ask to run

gradle clean check

on your PR code and fix them ? Thank you a lot

@leocck
Copy link
Contributor Author

leocck commented Nov 16, 2021

@mirkoperillo thanks for looking into that, i always forget about the gradle check 🤦🏼

Fixed now

@mirkoperillo
Copy link
Contributor

@leocck thank you, now PR is ready to be merged

@mirkoperillo mirkoperillo merged commit f95f76d into exercism:main Nov 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants