📜 ⬆️ ⬇️

Functional C #: Primitive obsession (primitive obsession)

This is the second article from the mini-series of articles about functional C #.


What is primitive obsession?


In short, this is when mainly primitive types (string, int, etc.) are used to simulate an application domain. For example, here’s how the Customer class might look like in a typical application:

public class Customer { public string Name { get; private set; } public string Email { get; private set; } public Customer(string name, string email) { Name = name; Email = email; } } 

The problem here is that if you need to enforce some business rules, you have to duplicate the validation logic throughout the class code:
')
 public class Customer { public string Name { get; private set; } public string Email { get; private set; } public Customer(string name, string email) { // Validate name if (string.IsNullOrWhiteSpace(name) || name.Length > 50) throw new ArgumentException(“Name is invalid”); // Validate e-mail if (string.IsNullOrWhiteSpace(email) || email.Length > 100) throw new ArgumentException(“E-mail is invalid”); if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”)) throw new ArgumentException(“E-mail is invalid”); Name = name; Email = email; } public void ChangeName(string name) { // Validate name if (string.IsNullOrWhiteSpace(name) || name.Length > 50) throw new ArgumentException(“Name is invalid”); Name = name; } public void ChangeEmail(string email) { // Validate e-mail if (string.IsNullOrWhiteSpace(email) || email.Length > 100) throw new ArgumentException(“E-mail is invalid”); if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”)) throw new ArgumentException(“E-mail is invalid”); Email = email; } } 

Moreover, exactly the same code tends to fall into the application layer:

 [HttpPost] public ActionResult CreateCustomer(CustomerInfo customerInfo) { if (!ModelState.IsValid) return View(customerInfo); Customer customer = new Customer(customerInfo.Name, customerInfo.Email); // Rest of the method } public class CustomerInfo { [Required(ErrorMessage = “Name is required”)] [StringLength(50, ErrorMessage = “Name is too long”)] public string Name { get; set; } [Required(ErrorMessage = “E-mail is required”)] [RegularExpression(@”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”, ErrorMessage = “Invalid e-mail address”)] [StringLength(100, ErrorMessage = “E-mail is too long”)] public string Email { get; set; } } 

Obviously, this approach violates the principle of DRY . This principle tells us that every part of the domain information should have a single authoritative source in the code of our application . In the example above, we have 3 such sources.

How to get rid of obsession with primitives?


To get rid of obsession with primitives, we have to add two new types that aggregate the validation logic. This way we can get rid of duplication:

 public class Email { private readonly string _value; private Email(string value) { _value = value; } public static Result<Email> Create(string email) { if (string.IsNullOrWhiteSpace(email)) return Result.Fail<Email>(“E-mail can't be empty”); if (email.Length > 100) return Result.Fail<Email>(“E-mail is too long”); if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”)) return Result.Fail<Email>(“E-mail is invalid”); return Result.Ok(new Email(email)); } public static implicit operator string(Email email) { return email._value; } public override bool Equals(object obj) { Email email = obj as Email; if (ReferenceEquals(email, null)) return false; return _value == email._value; } public override int GetHashCode() { return _value.GetHashCode(); } } public class CustomerName { public static Result<CustomerName> Create(string name) { if (string.IsNullOrWhiteSpace(name)) return Result.Fail<CustomerName>(“Name can't be empty”); if (name.Length > 50) return Result.Fail<CustomerName>(“Name is too long”); return Result.Ok(new CustomerName(name)); } //     ,  Email } 

The advantage of this approach is that in the event of a change in the validation logic, we only need to reflect this change only once.

Note that the Email class constructor is closed, so the only way to create an instance of it is to use the static Create method, which performs all the necessary validations. This approach allows us to be confident that all instances of the Email class are in a valid state throughout their lives.

Here is how the controller can use these classes:

 [HttpPost] public ActionResult CreateCustomer(CustomerInfo customerInfo) { Result<Email> emailResult = Email.Create(customerInfo.Email); Result<CustomerName> nameResult = CustomerName.Create(customerInfo.Name); if (emailResult.Failure) ModelState.AddModelError(“Email”, emailResult.Error); if (nameResult.Failure) ModelState.AddModelError(“Name”, nameResult.Error); if (!ModelState.IsValid) return View(customerInfo); Customer customer = new Customer(nameResult.Value, emailResult.Value); // Rest of the method } 

Instances of Result <Email> and Result <CustomerName> explicitly tell us that the Create method can fail, and if so, we can find out the reason by reading the Error property.

Here is how the Customer class looks after refactoring:

 public class Customer { public CustomerName Name { get; private set; } public Email Email { get; private set; } public Customer(CustomerName name, Email email) { if (name == null) throw new ArgumentNullException(“name”); if (email == null) throw new ArgumentNullException(“email”); Name = name; Email = email; } public void ChangeName(CustomerName name) { if (name == null) throw new ArgumentNullException(“name”); Name = name; } public void ChangeEmail(Email email) { if (email == null) throw new ArgumentNullException(“email”); Email = email; } } 

Almost all checks moved to Email and CustomerName. The only remaining validation is a null check. We will see how to get rid of it in the next article.

So, what are the benefits of getting rid of obsession with primitives?


A small note. Some developers tend to "wrap" and "deploy" primitive types several times during a single operation:

 public void Process(string oldEmail, string newEmail) { Result<Email> oldEmailResult = Email.Create(oldEmail); Result<Email> newEmailResult = Email.Create(newEmail); if (oldEmailResult.Failure || newEmailResult.Failure) return; string oldEmailValue = oldEmailResult.Value; Customer customer = GetCustomerByEmail(oldEmailValue); customer.Email = newEmailResult.Value; } 

It is best to use custom types in the entire application, expanding them into primitives only when they go beyond the boundaries of the domain, for example, are saved to the database or rendered in HTML. In your domain classes, try to always use custom types, the code in this case will be simpler and more readable:

 public void Process(Email oldEmail, Email newEmail) { Customer customer = GetCustomerByEmail(oldEmail); customer.Email = newEmail; } 

Restrictions


Unfortunately, the creation of wrapper types in C # is not so simple as in F #, for example. This may change in C # 7 if pattern matching and record types are implemented at the language level. Until then, we have to deal with the clumsiness of this approach.

Because of this, some primitive types are not worth being wrapped. For example, the type of "money amount" with a single invariant, indicating that the amount of money can not be negative, can be represented as a regular decimal. This will lead to some duplication of validation logic, but even so, this approach will be a simpler solution, even in the long run.

As usual, stick to common sense and weigh the pros and cons of solutions in each case.

Conclusion


With immutable and non-primitive types, we come closer to designing C # applications in a more functional style. In the next article, we will discuss how to mitigate the billion dollar mistake.

Sources



The remaining articles in the cycle



English version of the article: Functional C #: Primitive obsession

Source: https://habr.com/ru/post/266937/


All Articles