DEV Community

Cover image for Refactoring Currency: Mastering Universal Currency Value Objects with Enums and Attributes
Kostiantyn Bilous for SharpAssembly

Posted on • Updated on

Refactoring Currency: Mastering Universal Currency Value Objects with Enums and Attributes

Original article on Medium

Precision is paramount in financial software development, where even seemingly minor components can have major implications. Among these, the representation of Currency stands as a foundational element that demands meticulous attention. This article delves into the art of refining Currency representation within financial systems. By adopting Domain-Driven Design (DDD) principles, we'll explore how to transform Currency from a basic Entity into a well-defined Value Object. Discover how crafting robust and universal Currency Value Objects can subtly yet significantly enhance the clarity and reliability of financial applications.

Theoretical Underpinnings: Entities and Value Objects in Domain-Driven Design

In Domain-Driven Design (DDD), understanding the distinction between Entities and Value Objects is fundamental. Entities are defined by their identity, while Value Objects are characterized by their attributes. Often misconstrued as a mere attribute, Currency holds substantial complexity, primarily when interacting with different system parts. It could be misconceptually implemented as an Entity in the Bounded Context, where it naturally behaves as a Value Object.

Why Refactor Currency into a Value Object?

  • Immutability: Value Objects are immutable. This immutability aligns perfectly with the nature of the Currency, where the value shouldn't change once instantiated.
  • Validation and Consistency: Encapsulating the currency as a Value Object allows centralized validation and business rule enforcement, ensuring Currency consistency across the system.
  • Reduction of Complexity: Representing Currency as a Value Object simplifies operations, abstracting the intricacies of currency handling into well-defined methods.

Practical Application: The Refactoring Process

Let's dive into the practicalities using an example from the InWestMan application. This application is designed for personal investment tracking and uses Money as a Value Object.

Original Scenario: Currency as an Entity
Initially, the Currency in our system was an Entity laden with unnecessary complexity and potential for inconsistencies.

public class Currency : BaseEntity<int> 
{
    public string Code { get; set; }
    public string Name { get; set; }
    public int FractionDigits { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

This Currency is used in theMoney class, which is an Enterprise Pattern commonly used in Domain-Driven Design.

Link to Article on Money Patterns:
Money Patterns in Domain-Driven Design: Navigating the Complexities of Financial Systems

public sealed class Money : ValueObject
{
    private readonly decimal _amount;
    private readonly Currency _currency;

    public Money(decimal amount, Currency currency)
    {
        _amount = amount;
        _currency = currency;
    }

    public Currency Currency => _currency;

    public decimal Amount => decimal.Round(_amount, _currency.FractionDigits, MidpointRounding.ToEven);

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency.Code;
    }

    // Methods
}
Enter fullscreen mode Exit fullscreen mode

Refactoring Steps:
1. Defining the Immutable Enum: We define a new CurrencyCode enum (e.g. ISO 4217), encapsulating all relevant properties and behaviors in its attributes.

public enum CurrencyCode
{
    [EnumMember(Value = "USD")] 
    [Description("United States dollar")] 
    [FractionDigit(2)]
    USD = 840,

    [EnumMember(Value = "UAH")] 
    [Description("Ukrainian hryvnia")] 
    [FractionDigit(2)]
    UAH = 980,

    [EnumMember(Value = "EUR")] 
    [Description("Euro")] 
    [FractionDigit(2)]
    EUR = 978

    ...
}
Enter fullscreen mode Exit fullscreen mode

2. Storing Data in Memory as Attributes: We ensure that instances of CurrencyCode contain all relevant data.

[AttributeUsage(AttributeTargets.Field)]
public class FractionDigitAttribute : Attribute
{
    public FractionDigitAttribute(int fractionDigit)
    {
        FractionDigit = fractionDigit;
    }

    public int FractionDigit { get; private set; }
}
Enter fullscreen mode Exit fullscreen mode

3. Centralizing Business Logic: Currency-related data (like full name, fraction digits, etc.) are centralized within the CurrencyCode enum, promoting code reusability and maintainability by static extensions.

public static class CurrencyCodeExtensions
{
    public static int GetFractionDigit(this CurrencyCode currencyCode)
    {
        var type = currencyCode.GetType();
        var memInfo = type.GetMember(currencyCode.ToString());
        if (memInfo.Length > 0)
        {
            var attributes = memInfo[0].GetCustomAttributes(typeof(FractionDigitAttribute), false);
            if (attributes.Length > 0)
                return ((FractionDigitAttribute)attributes[0]).FractionDigit;
        }

        return 2;
    }
}
Enter fullscreen mode Exit fullscreen mode

In the context of our Money class, the refactoring leads to more transparent, more concise operations:

public sealed class Money : ValueObject
{
    private readonly decimal _amount;
    private readonly CurrencyCode _currencyCode;
    private readonly int _fractionDigits;

    public Money(decimal amount, CurrencyCode currencyCode)
    {
        _amount = amount;
        _currencyCode = currencyCode;
        _fractionDigits = _currencyCode.GetFractionDigit();
    }

    public decimal Amount => decimal.Round(_amount, _fractionDigits, MidpointRounding.ToEven);

    public CurrencyCode CurrencyCode => _currencyCode;

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Amount;
        yield return CurrencyCode;
    }

    // Other methods 
}
Enter fullscreen mode Exit fullscreen mode

Conclusion: The Strategic Impact of Refactoring

The transition from treating Currency as an Entity to a Value Object in financial software isn't merely a technical refactor but a strategic enhancement. It aligns the codebase with the principles of Domain-Driven Design, promoting clarity, consistency, and maintainability. This refactoring journey exemplifies how thoughtful changes in code structure can lead to more robust, reliable, and scalable financial systems.

Original article on Medium

Stay tuned for more insights and detailed analyses, and feel free to share your thoughts or questions in the comments below!

SharpAssembly on Dev.to
SharpAssembly on Medium
SharpAssembly on Telegram

Cover credits: DALL·E generated

Top comments (0)