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

[WIP] [experimental] Interfaces for polymorphic API resources #1313

Conversation

ob-stripe
Copy link
Contributor

An experiment to see if we could replace classes like ExternalAccount and PaymentSource (which don't really exist in the API) by interfaces.

iamcarbon and others added 30 commits September 25, 2018 07:49
Make internals visible to Newtonsoft.Json
Cross target netstandard2.0
This ensures that params/class structure matches the API. Also changed
DateFilter to match the API and remove custom encoding and renamed it to
DateRangeOptions
Also made a few changes to some resources to match the API naming and added
support for AddressJapan on the Account resource.
@ob-stripe ob-stripe changed the title [WIP] [experimental] Replace [WIP] [experimental] Replace intermediary classes by interfaces Oct 1, 2018
Copy link
Contributor

@remi-stripe remi-stripe left a comment

Choose a reason for hiding this comment

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

So this looks really neat in concept. I really like it. I am just a little unsure about removing the Type property on the ExternalAccount class. I know why you did it but is using GetType really common enough that people would prefer that over an explicit type check?

EDIT: I realize that my sentence is confusing. GetType is an explicit type check. I meant is it more common than doing if externalAccount.Type == "bank_account" ... or whatever?

@ob-stripe ob-stripe force-pushed the ob-polymorphic-resource-interfaces branch from 48462ff to e172fd7 Compare October 2, 2018 09:23
@ob-stripe
Copy link
Contributor Author

I think calling GetType is the standard way of checking the concrete type of an "interface instance".

I also just discovered the dynamic keyword which can be used instead of var to tell the compiler that the actual type is not known until runtime, and prevents the need for casting entirely. I've updated the tests to use it.

@ob-stripe ob-stripe force-pushed the ob-polymorphic-resource-interfaces branch from e172fd7 to 1e48083 Compare October 2, 2018 09:27
@ob-stripe ob-stripe changed the title [WIP] [experimental] Replace intermediary classes by interfaces [WIP] [experimental] Interfaces for polymorphic API resources Oct 2, 2018
Copy link
Contributor

@remi-stripe remi-stripe left a comment

Choose a reason for hiding this comment

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

Added a few more comments, mostly for my own education

{
Id = incoming.SelectToken("id").ToString()
};
var externalAccount = default(IExternalAccount);
Copy link
Contributor

Choose a reason for hiding this comment

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

didn't you say you did not need to use var?

Copy link
Contributor Author

@ob-stripe ob-stripe Oct 2, 2018

Choose a reason for hiding this comment

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

To clarify: dynamic can (but it's not a requirement) be used to tell the compiler that the exact type won't be known until runtime. It's not a silver bullet, because it disables compile-type checks. E.g. if I write this:

dynamic foo = account.ExternalAccounts.Data[0];
Console.WriteLine(foo.AccountHolderName)

The compiler has no way of knowing whether AccountHolderName is a valid call. If it's not (because foo is a Card instead of a BankAccount), a runtime exception will be raised.

A safer but more verbose alternative:

var foo = account.ExternalAccounts.Data[0]; // foo is an `IExternalAccount`
if (foo is BankAccount)
{
  Console.WriteLine(((BankAccount)foo).AccountHolderName); // need to manually cast to BankAccount
}
else if (foo is Card)
{
  Console.WriteLine(((Card)foo).Name); // need to manually cast to Card
}
else
{
  // this should never be reached, unless a new concrete type is introduced for IExternalAccount
}

In the specific place of the code, we can use var because we don't need to access any of specific properties of the concrete types. We could replace var with IExternalAccount an it would be a no-op (it's the type that is inferred by the compiler).

Copy link
Contributor

Choose a reason for hiding this comment

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

Ack thanks for the thorough explanation. So the logic becomes similar to what we do in stripe-java. More verbose but safer and cleaner.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the lurk, but +1 the choice to avoid dynamic if possible. It's one of these tools that can be useful in a huge variety of situations, but probably should be avoided in almost all of them because of loss of typedness is a very significant drawback (no more compiler checks, Intellisense, etc.).


public static class Mapper<T>
{
private static JsonConverter[] converters = {
new ExternalAccountConverter(),
Copy link
Contributor

Choose a reason for hiding this comment

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

so this is the new place where we would list all converters?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. Not sure if this is the best way to achieve this, but it seems to work and should be relatively efficient (it's static, so only initialized once).

updateId(item.Id);
updateObject(item);
}
else if (value is string)
{
updateId((string)value);
updateObject(null);
updateObject(default(T));
Copy link
Contributor

Choose a reason for hiding this comment

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

can you clarify what this line does/changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In practice, nothing. default(T) returns the default value for the type T. For reference types, the default value is null.

However, since T is now constrained to be an interface rather than a class, the compiler complains that it cannot convert from null to T. This is (I think) because structs are value types but can implement interfaces.

{
public static void Map(object value, Action<string> updateId, Action<T> updateObject)
{
if (value is JObject)
{
T item = ((JToken)value).ToObject<T>();
// T item = ((JToken)value).ToObject<T>();
T item = Mapper<T>.MapFromJson(value.ToString());
Copy link
Contributor

Choose a reason for hiding this comment

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

Not fully sure what all of this does

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It uses our custom deserialization instead of the default one, to ensure that our registered converters are used.

Assert.Equal("bank_account", externalAccount.BankAccount.Object);
Assert.IsType<BankAccount>(externalAccount);
var bankAccount = (BankAccount)externalAccount;
Assert.Equal("bank_account", bankAccount.Object);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same you mentioned you did not need to use var anymore

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 forgot to update this test. But as said above, using dynamic is optional, it's basically syntactic sugar at the cost of type safety.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking about this a bit more, I think we could make Object part of the IStripeEntity interface (or maybe on a new IStripeEntityWithObject interface) so that we can check its value without using dynamic.

@@ -20,6 +20,7 @@
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net452' ">
<Reference Include="Microsoft.CSharp" />
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this exactly?

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 is needed to use dynamic with .NET Framework (at least for version 4.5.2). Note that this is only added in the tests project. Users of the library who use .NET Framework and want to use dynamic would need to do the same thing, but the library doesn't make this a requirement.

@ob-stripe ob-stripe force-pushed the ob-polymorphic-resource-interfaces branch from 1e48083 to 9eaf9b8 Compare October 2, 2018 14:46
@ob-stripe
Copy link
Contributor Author

Closing this one because rebasing is a pain.

@ob-stripe ob-stripe closed this Oct 4, 2018
@ob-stripe ob-stripe deleted the ob-polymorphic-resource-interfaces branch October 4, 2018 16:13
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.

4 participants