Tuesday, September 1, 2015

Functional C #: Obsession primitives

We continue the series of articles on functional programming in C #:


Functional C #: Immutable objects
Functional C #: Obsession primitives
Functional C #: Non-zero reference types
Functional C #: Exception Handling
Imagine that you were to describe a class Customer, containing certain information - for example, the name and e-mail. Unfortunately, many programmers think that to use for the name and address of the field of elementary data types is much easier than to write a base class.

If you think so, too, the result will be similar to your code:

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;
    }
}
This method is the right one exactly to the moment when you will not need to check the values ​​of the fields for correctness. But you are not so easy to convince you write a lot of checks. As a result, the class increases and acquires different conditions:

public class Customer
{
    public string Name {get; private set; }
    public string Email {get; private set; }
    public Customer (string name, string email)
    {
        // Validation name
        if (string.IsNullOrWhiteSpace (name) || name.Length> 50)
            throw new ArgumentException ("Name is invalid");
        // Validation 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)
    {
        // Validation name
        if (string.IsNullOrWhiteSpace (name) || name.Length> 50)
            throw new ArgumentException ("Name is invalid");
        Name = name;
    }
    public void ChangeEmail (string email)
    {
        // Validation 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;
    }
}
Yeah, okay, if these checks were within the class. But they go to the next level - the main application class:

[HttpPost]
public ActionResult CreateCustomer (CustomerInfo customerInfo)
{
    if (! ModelState.IsValid)
        return View (customerInfo);
    Customer customer = new Customer (customerInfo.Name, customerInfo.Email);
    // The 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; }
}
You will agree that such an approach, to put it mildly, is not quite correct. But what about the DRY principle and a single source of truth? In the example above, at least three of such source, which is not justified.

This situation is called the state of primitive obsession. In the next chapter we will show you how to work around this "disease".

How to get rid of obsession primitives?
Very simple! We only need to enter two new classes, which we will check the validity of the values. This will be a single source of truth, which was mentioned above.

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 not 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 not be empty");
        if (name.Length> 50)
            return Result.Fail <CustomerName> ("Name is too long");
        return Result.Ok (new CustomerName (name));
    }
    // Next will be the same as in the class Email
}
The beauty of this approach is that if you want to change the validation logic values, you will need to adjust the code in exactly the same place. The less duplication of code in your program, the fewer mistakes you make and the happier your customers!

Note that the constructor Email private. A new instance, we can create with the help of the method Create, which runs the input value through a variety of filters, checking for validity. This is done in order to correct the value of the object was from the very beginning of its existence.

Here is an example of the use of 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);
    // The rest of the method
}
Please note that items Result <Email> and Result <CustomerName> explicitly tell us that the Create method might cause mistakes. And if that happens, the error information can be found in the properties of Error.

Now let's take a look at Customer class after we have introduced two small side-Class:

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 the checks have been moved to classrooms Email and CustomerName. Only the condition with checks for null, but we will consider in the next article.

So what are the benefits we got rid of obsession primitives?

We have created a single authoritative source of knowledge for each object, and got rid of the duplication of code.
Now it is impossible to assign an object of error Email or CustomerName a value that would lead to a compiler error.
No need for additional validating e-mail address or the name of the buyer. If the objects of class CustomerName Email or there, then we know that the data stored in them is absolutely true.
There is one detail, which would like more detail. The fact that some programmers get rid of the obsession with primitive types is not complete. For example:

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 must be remembered that the use of the basic types of costs only when the object leaves the program. That is, in cases where the values ​​are entered into the database, or exported to an external file. But in your application, try to use you write wrapper classes as often as possible. This will make your code more clear. See for yourself:

public void Process (Email oldEmail, Email newEmail)
{
    Customer customer = GetCustomerByEmail (oldEmail);
    customer.Email = newEmail;
}
The downside: limited
Unfortunately, the creation of custom data types implemented in C # is not as perfectly as in functional languages: F #, for example. Perhaps the situation will correct a new version of the language: C # 7.0.

That is why I believe that in some situations the use of primitives better than to create a simple wrapper class. For example, to represent money. They can be expressed using the elementary data type with only one test mark number. Yes, you will need to duplicate this condition, but this decision easier, even in the long term.

As always, I will tell you that you before you write something, weigh the "pros" and "cons", and only then make a decision. And do not be afraid to change his mind a few times.

Conclusion
With unchanged and primitive types of data we get closer and closer to solving the problems in the paradigm of functional programming. In the next article we will try to get rid of numerous checks on null.

By Unknown with No comments

0 коммент.:

Post a Comment