-
-
Notifications
You must be signed in to change notification settings - Fork 710
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
Add a Reflection proxy utility class #2053
Conversation
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
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 :) |
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). |
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? |
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 I like the idea and implementation. |
* An instance of the target class (if found) | ||
*/ | ||
private Object target; | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; | ||
} | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were there two constructors?
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.*; | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
return false; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 🙏
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Yay! Thanks for your review, I fixed all the issues you pointed out. |
Great idea! I'm planning to do something similar for the C# track. |
@mirkoperillo The build action: |
@ericbalawejder you have right. Currently when the CI workflow starts the java/.github/workflows/gradle.yml Line 23 in acfdd2b
|
There was a problem hiding this 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
@mirkoperillo thanks for looking into that, i always forget about the gradle check 🤦🏼 Fixed now |
@leocck thank you, now PR is ready to be merged |
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