-
-
Notifications
You must be signed in to change notification settings - Fork 101
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
Remove final definition from classes #38
Comments
Hi, classes a final for two reasons:
I'm a bit torn regarding your use case; usually I'd advise you to create your own Money class, wrapping the original Money instance, and forwarding method calls. You could then add whatever interface you need on top of it; but just to add an interface this seems like a big burden, I get it. Let me think about it a bit more. I'd be interested to know what other people think as well, if anyone's listening. |
Hey Ben, Thanks for the quick reply. Understood. Well, I will leave it up to you! Here's a few extra thoughts, just in case it matters:
Due to the above, I'll proceed with forking the package and adjusting as needed. If you decide to remove the final designation within your version of the package, then it'll be easy to switch back over. Cheers! P.S. Feel free to close this issue at any time if no one else provides input soon. :-) |
Thanks for the write up! Note that this change would imply changing all |
I prefer to wrap third party VOs in my own namespace and treat them as a implementation detail. Following a real word example from an older project of mine. The underlying <?php
declare(strict_types=1);
namespace App\Domain;
use JsonSerializable;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\DecimalMoneyFormatter;
use Money\Money as Moneyphp;
use Money\Parser\DecimalMoneyParser;
final class Money implements JsonSerializable
{
private Moneyphp $money;
/**
* @param string $currencyCode ISO 4217 Currency code
*/
public static function create(string $amount, string $currencyCode): self
{
$currencies = new ISOCurrencies();
$moneyParser = new DecimalMoneyParser($currencies);
$money = $moneyParser->parse($amount, $currencyCode);
return new self(
$money
);
}
private function __construct(Moneyphp $money)
{
$this->money = $money;
}
public function getCurrencyCode(): string
{
return $this->money->getCurrency()->getCode();
}
public function add(self $other): self
{
return new self($this->money->add($other->money));
}
public function equals(self $other): bool
{
return $this->money->equals($other->money);
}
public function jsonSerialize(): array
{
$currencies = new ISOCurrencies();
$moneyFormatter = new DecimalMoneyFormatter($currencies);
return [
'amount' => $moneyFormatter->format($this->money),
'currency' => $this->money->getCurrency(),
];
}
} |
Just to give my two cents: I fully agree with that statement and try to abstract my dependencies away adding functionality around it as needed. |
Couldn't this be mitigated to a large extent by using private internal methods (as opposed to protected)? I could only see one protected method from a quick look through, so probably isn't a big change. The only thing that would then be visible to child classes would be the public API. Arguably if this is the case you 'should' be using composition, but in the case of Arrayable etc it does feel like a lot of boilerplate code (unless you're using magic methods, which feels a bit nasty). There are of course other compatibility considerations with inheritance, but an 'extend at your own risk' note in the readme may suffice. Personally, I don't have strong feelings either way. |
Bump on this one. Although this package is pure gold, that final keyword in this package overcomplicates the extending process creates some sort of authority on how package consumers are supposed to work with the code. Any chance you could remove final keyword and add In my use case, I just need a proper type-hinting (late static binding) and a couple of additional methods to make development easier. |
I'd also gladly rollout a PR with the update. |
Created a fork and made a pull request: #52 Main arguments to make the Money class extendable are these:
Let know if more arguments / discussions are needed? |
@sybbear Would you mind sharing the extra methods you're adding to the |
I'd like to add a few methods, that would allow me to format money in a specific way, depending on a country and use case. For example:
There is for example a Carbon is not final and is open for extension. It would be a breeze to follow a similar pattern. |
The more precise explanation of the above example is actually PHP's DateTime class, which is in my opinion a value object. Carbon then extends it and augments with intuitive methods. |
@sybbear This is actually a very good example of what I think people should not do with What I would suggest in this case is:
class MoneyFormatter
{
public function __construct(private string $defaultLocale)
{
// ...
}
public function formatWithSign(Money $money): string
{
// ...
}
public function formatFloat(Money $money)
{
// ...
}
} Regarding This is what I did in brick/date-time when I needed functionality that was provided by |
Hello there 🙌 And back to the discussion. And I do not disagree, that MoneyFormatter approach is a good one, but in our specific case I do not find it reasonable to create a new class every time we need more methods. I may need to add more methods like Another argument: This post has a good discussion, both sides (also touches topic about open source libs, frameworks) I see that you prefer one way of coding over another one and I see how final keyword has a value per project basis (it's wise and valid), but what is the need to enforce it in a public open-source package? 😀 I assume consumers of this package would like to have the actual functionality and not additional religion/opinions/views on how to structure THEIR code, because only they know better their needs and specs of their projects. |
Thanks a lot for the support, @sybbear! I really appreciate it. I just read the article & discussion on Matthias Noback's blog, and this only reinforced my opinion that
I'm not very sensitive to the argument of being able to fix a bug by subclassing. If you have a fix for a bug and submit it as a GitHub PR, it will likely be merged & released within 24 hours on my projects. If you really need it now, then just use your fork temporarily in The main argument against removing I'll keep this issue open a little while to let things settle down and give me a chance to change my opinion, but I'm relatively confident that the current way is the right way! |
Thank you for reply @BenMorel I could imagine, if there would be a team of >10 people in one team and final keyword would communicate that "Hey, this class is supposed to work this way and this way only. Not for extension." Or "This class is still raw and I'm working on it". There is a possibility to contact a teammate and ask the exact reason why the class is final. But I just cannot wrap my head around as to why having final in this repository is important when it is an open-source package and there is pretty much only person behind the development. So I'd really like to hear a practical example, where not having a final keyword is a maintenance burden. Such an example would be seeing an unstoppable stream of pull requests with As for backwards compatibility, semantic versioning is the way of doing it, which is pretty much a standard nowadays, also given the fact that the package is being installed via composer. Developers are supposed to lock to a specific version and update dependencies consciously, trusting that maintainers follow the semantic versioning constraints. As for now, there will be just a number of forks (I found 3 already) that remove the final keyword and they will be lacking automated updates, because they need to structure the code their own way. Anyways, these are my two cents on the topic. |
The issue here is the scope of the backwards compatibility – if you allow subclassing you should arguably ensure that all inheritable (i.e. protected) methods and properties are backwards compatible. Personally I wouldn't go that far, but I can see the merit of being explicit, especially on a popular package. If you're being strict about it, the package may have to release a new major version when any new method is added, in case a user has already added that method in a subclass with a conflicting signature. For example, brick/money adds |
@rdarcy1 More pieces start to fall in place, so I see what's the idea behind final here. (Also it's time to start using a static code analyzer like https://github.com/phan/phan (scans method signatures), just in case if some packages don't follow that kind of SemVer 😄) |
What do you think of adding |
This would be useful for overriding the default context and rounding mode as well. I'd like to change that for my whole app to a custom context, and HALF_UP rounding mode. I do all my calculations in rational, and I find it annoying that I have to specify these always when converting it to Money, or creating a new Money. |
@sybbear mind updating your fork to latest main, would need it in a Laravel project with some additional methods 🙏 |
just for reference, a implementation capsulating |
@simonbuehler This is a good example of how you can encapsulate IMO this is the preferred way to augment the capabilities of Money. |
Hi, jumping in the conversation. |
I agree on @steven-fox on this one - again :) Also, with all respect @BenMorel , how would you suggest using composition instead of extension, if Money's methods are documented to return same Money and not static (current extension type or even mixining proxy class). And because it's final, we'd have to declare-override return type in a separate proxy decorator class for every single method to achieve this. I mean, you cannot do method chaining with un-extended methods, because they point back to base class Money and not the actual extension. Are you planning any time soon to upgrade return types to static? |
I'm not planning to remove the final keyword from classes at the moment. As explained before, composition should be favoured over inheritance if you're planning to extend I know this may sound cumbersome, but it is IMO the correct way to "extend" a class you don't control. Extendability brings its own set of problems, that I don't want to have to deal with as a maintainer, most notably more surface for BC breaks. |
I might miss some points but doesn't a pattern like this public function __call(string $name, array $arguments)
{
if (! method_exists($this->money, $name)) {
throw new RuntimeException(
message: "Method [$name] does not exist.",
);
}
return $this->money->{$name}(...$arguments);
} Allow for calling all methods of the capsulated object and adding own extension methods in the class quite easily? |
No pun intended, do you guys use Notepad++ for coding? So we can do One can see, why you don't want to remove |
And apologies once again for bumping this issue in a hard-ish way, but hopefully you can also see why it is frustrating:
And at the same time: Hopefully you understand this point of view and give your package ability to be used more extensively -> and therefore more developers will use the package and donate resources, it's really in your interest if you think about it :) |
I'm sorry this reply will fall under "getting coached on software dev patterns", but I definitely consider If anything, I'd be less strict about replacing every
Potentially agree, if you consider that extendability is not limited to
The aim of this library is to provide the foundations to support all countries/currencies. What is it missing to support this use case? Shouldn't this be merged in
Brick/money already supports cryptos through custom Finally, if you really need to decorate |
@BenMorel Let’s proceed to close this issue to make your life easier. You make perfectly good arguments that favor keeping things the way they are. No need to add more comments to this issue, taking up more of your time, in my eyes. For those that want to extend, pick one of the options and move forward. If you desperately want to change something that doesn’t jive with Ben’s approach, maintain your own fork. Easy peasy lemon squeezy. Happy coding everyone. 👍 |
This is the reason why I'm gonna make my own money lib for the next project I need one. The other money lib is weird to use with instanciating money objects with minor amounts by default, eg This one is closed to the point that I would need to decorate almost every single method to use, and at that point I prefer to write my own. |
How to solve the below "problem":
Sure I can store |
Thanks for this great library. But I don't get why you don't want to change While this is your code base and you are free to do whatever you want, I just don't get the advantage for you. Would be thankful if you could consider this change again. |
Just a few ideas about reaching a compromise:
|
Good afternoon,
I was wondering if we could safely drop the
final
definition from the classes within the package. I understand the value of defining certain classes as such, but I could see several valid use cases where extending theMoney
class, for example, would make sense.As a current use case, I'd like to do this in a Laravel app to add the Arrayable and JsonSerializable interfaces to the Money class. I don't think this should be a part of this package, so it makes more sense to permit extension by package consumers.
Without the ability to extend, I'll have to throw together a factory and forward calls using magic methods. Yuk. Or make a fork. Also seems unnecessary.
If there is a particular reason why the classes are defined as final in the package, I'd appreciate a quick explanation so that I can decide how to proceed from there.
Thanks for your time.
The text was updated successfully, but these errors were encountered: