Thursday, February 16, 2017

Modifiable read-only interface

Read-only interface is a simple thing. It does not allow user to change its state. But sometimes you may want to "change" it. I'm not talking about actual modification of state of object. I'm talking about creation of NEW object which state is almost the same as the old one.

Let's consider simple read-only interface:

public interface IReadOnlyPerson
{
    string FirstName { get; }
    string LastName { get; }
}

I have an object of this type:

IReadOnlyPerson person;

How can I get another object of type IReadOnlyPerson which has the same values of all properties as 'person' object except one of them (e.g. 'FirstName')? Let's see how we can solve this task.

Implementation of modifiable read-only interfaces


First of all I must say that there should be only one class implementing modifiable read-only interface:

public class Person : IReadOnlyPerson
{
    public string FirstName { getset; }
    public string LastName { getset; }
}

Why only one? Because in this case you can be sure about type of object implementing this interface.

Next we'll extend the read-only interface with a method to get modified copy of the object:

public interface IReadOnlyPerson
{
    string FirstName { get; }
    string LastName { get; } 
    IReadOnlyPerson Clone(Action<Person> modifier = null);
}

As you can see this method accepts a method which can modify copy of our object. And as there is only one class implementing this interface we know exactly type of this object (Person).

Implementation of Clone method can be quite simple:

IReadOnlyPerson IReadOnlyPerson.Clone(Action<Person> modifier)
{
    var clone = new Person();
 
    clone.FirstName = FirstName;
    clone.LastName = LastName;
 
    modifier?.Invoke(clone);
 
    return clone;
}

I'm using explicit implementation of interface here to keep Intellisense list clean. But it is a matter of taste.

Now we can create modified copy of our read-only object:

IReadOnlyPerson readOnlyPerson = new Person
{
    FirstName = "John",
    LastName = "Smith"
};
 
var clone = readOnlyPerson.Clone(p => p.LastName = "Black");

From this code you can derive the second important  property of class implementing modifiable read-only interface: it should allow modification of each and every it's aspect. Only in this case we can modify any piece of it using single delegate passed to Clone method. Later in this article I'll explain how to do it. But now let me shed light on one more thing.

Our example of read-only interface is very simple. But in real situation you may want to create instances of this interface from very different data. It may be tempting to create two classes implementing one read-only interface like this:

public interface IReadOnlyData
{
    // ...
}
 
public class Data1 : IReadOnlyData
{
    public Data1(ExternalData1 externalData)
    {
        // parsing here...
    }
 
    // ...
}
 
public class Data2 : IReadOnlyData
{
    public Data2(ExternalData2 externalData)
    {
        // parsing here...
 
    }
 
    // ...
}

Please, resist this temptation if you want your read-only interface to be modifiable. Remember that there should be only one class implementing it. Instead use factories for producing instances of this class from different data:

public interface IReadOnlyData
{
    // ...
}
 
public class Data : IReadOnlyData
{
    // no parsing here
}
 
public class DataFactory1
{
    public IReadOnlyData CreateData(ExternalData1 externalData)
    {
        // parsing here...
 
    }
 
    // ...
}
 
 
public class DataFactory2
{
    public IReadOnlyData CreateData(ExternalData2 externalData)
    {
        // parsing here...
 
    }
 
    // ...
}

Now it is time to look closer at the implementation of our read-only interface.

Cloning of objects


First thing we should do before applying modifications is to make exact copy of existing object. We can certainly do it manually. But when number of properties of our objects grows then also grows possibility to forget something to copy. To solve this problem we can use AutoMapper. This package allows us to make deep copies of objects very easily:

Mapper.Map(person, clone);

This command copies all properties of object 'person' to object 'clone'. But before using this command we should configure it. It means that we should inform AutoMapper about all types of objects which we want to map (copy/clone).

You may initialize AutoMapper in one place:

Mapper.Initialize(cfg =>
{
    cfg.CreateMap<PersonPerson>();
    // other configurations...
});

This approach allows you to keep configuration of AutoMapper in one place. But if your interface is very complex and contains references to other interfaces it is easy to forget to add some class to the configuration, especially if you are adding new interface:

public interface IReadOnlyPerson
{
    string FirstName { get; }
    string LastName { get; }
    IReadOnlyAddress Address { get; }
    IEnumerable<IReadOnlyPhone> Phones { get; }
    IReadOnlyPerson Clone(Action<Person> modifier = null);
}

In this case you can make each of your classes to register itself in the AutoMapper configuration. For this purpose I created simple class for support of mapping:

public class ReadOnlyMapper
{
    public static readonly MapperConfigurationExpression Configurator = new MapperConfigurationExpression();
 
    public static IMapper GetMapper()
    {
        return new MapperConfiguration(Configurator).CreateMapper();
    }
}

In static constructor of every class I use in my interface I'll register it in AutoMapper:

public class Person : IReadOnlyPerson
{
    static Person()
    {
        ReadOnlyMapper.Configurator.CreateMap<PersonPerson>();
    }
 
    ...
 
    IReadOnlyPerson IReadOnlyPerson.Clone(Action<Person> modifier)
    {
        var clone = new Person();
 
        ReadOnlyMapper.GetMapper().Map(this, clone);
 
        modifier?.Invoke(clone);
 
        return clone;
    }
}

and in Clone method I'll use a mapper built on the configuration to copy my current object into a new one.

In the end it is up to you which approach to use.

Complex objects


It is a common scenario when read-only interface references not only simple data types (string, int, ...) but also other custom types:

public interface IReadOnlyPerson
{
    string FirstName { get; }
    string LastName { get; }
    IReadOnlyAddress Address { get; }
 
    IReadOnlyPerson Clone(Action<Person> modifier = null);
}
 
public interface IReadOnlyAddress
{
    string Country { get; }
    string City { get; }
}

In order to be able to modify also Address object inside Person object we must make it fully available for modification:

public class Person : IReadOnlyPerson
{
    static Person()
    {
        ReadOnlyMapper.Configurator.CreateMap<PersonPerson>();
    }
 
    public string FirstName { getset; }
    public string LastName { getset; }
    public Address Address { getset; }
 
    IReadOnlyAddress IReadOnlyPerson.Address => Address;
 
    IReadOnlyPerson IReadOnlyPerson.Clone(Action<Person> modifier)
    {
        var clone = new Person();
 
        ReadOnlyMapper.GetMapper().Map(this, clone);
 
        modifier?.Invoke(clone);
 
        return clone;
    }
}
 
public class Address : IReadOnlyAddress
{
    static Address()
    {
        ReadOnlyMapper.Configurator.CreateMap<AddressAddress>();
    }
 
    public string Country { getset; }
    public string City { getset; }
}

Here we also have only one implementation of IReadOnlyAddress interface: Address class. It allows complete modification of all its properties.

Class Person gives access directly to Address class instance, not only to IReadOnlyAddress instance. To avoid names collision I explicitly implement Address property of IReadOnlyAddress interface. It also allows me to keep list of Person class properties clean.

Now we can easily modify properties inside Address class:

var clone = readOnlyPerson.Clone(p => p.Address.City = "Moscow");

Collections in read-only interface


Our read-only interfaces can provide access to collections:

public interface IReadOnlyPerson
{
    string FirstName { get; }
    string LastName { get; }
    IEnumerable<IReadOnlyPhone> Phones { get; }
 
    IReadOnlyPerson Clone(Action<Person> modifier = null);
}

Here we should consider two cases separately. Actually IEnumerable interface can be implemented by collection class or by method.

Collections implemented by collection classes 


Let's say that our property returning IEnumerable is actually implemented by List or Array or something as simple as them. This case does not differ from case of complex objects very much. Class implementing our read-only interface should give access to the underlying collection. The only thing to remember is that this should not be collection of read-only interfaces but collection of classes implementing this interface. Instead of providing access to List<IReadOnlyPhone> we should provide access to List<Phone> where Phone implements IReadOnlyPhone interface:

public class Person : IReadOnlyPerson
{
    static Person()
    {
        ReadOnlyMapper.Configurator.CreateMap<PersonPerson>();
    }
 
    public string FirstName { getset; }
    public string LastName { getset; }
 
    public List<Phone> Phones { getset; }
 
    IEnumerable<IReadOnlyPhoneIReadOnlyPerson.Phones => Phones;
 
    IReadOnlyPerson IReadOnlyPerson.Clone(Action<Person> modifier)
    {
        var clone = new Person();
 
        ReadOnlyMapper.GetMapper().Map(this, clone);
 
        modifier?.Invoke(clone);
 
        return clone;
    }
}

In this case modification of person's phones will look like this:

var clone = readOnlyPerson.Clone(p =>
{
    p.Phones.Add(new Phone { Number = "123-456-78" });
});

Collections implemented by method


In some cases instances of IEnumerable can be implemented by methods. For example, the method can return objects from database using some ORM like Entity Framework. In this case class implementing our read-only interface should provide access to this method via read-write delegate property:

public interface IReadOnlyPerson
{
    string FirstName { get; }
    string LastName { get; }
    IEnumerable<IReadOnlyService> Services { get; }
 
    IReadOnlyPerson Clone(Action<Person> modifier = null);
}
 
public class Person : IReadOnlyPerson
{
    // ... other code 
    public Func<PersonIEnumerable<IReadOnlyService>> ServicesProvider { getset; }
 
    IEnumerable<IReadOnlyService> IReadOnlyPerson.Services => ServicesProvider?.Invoke(this);
}

First of all I'd like to stress your attention on the signature of this property. We need to provide IEnumerabe. It may look like Func<IEnumerabe> would be sufficient. The problem is that AutoMapper can't clone delegates. It just leave it as is. It means that in the cloned object our delegate will still be the same as in the source object. It is not a problem if the method wrapped in the delegate does not use our Person class. But if it does then it will use source object, not cloned object. This is why we explicitly send instance of current object to the method. The method should use only this reference if needed.

This approach will allow us to replace existing method with a new one:

var clone = readOnlyPerson.Clone(p =>
{
    var oldServicesProvider = p.ServicesProvider;
    p.ServicesProvider =
        (pInst) =>
            new[] {new Service {ServiceId = "TimeProvider"}}.Concat(oldServicesProvider?.Invoke(pInst) ??
                                                            new Service[0]);
});

Weakly typed collections


Sometimes we have to live with weak typing. Consider the following example:

public interface IReadOnlyServiceProvider
{
    T GetService<T>();
 
    IReadOnlyServiceProvider Clone(Action<ServiceProvider> modifier = null);
}

If method GetService returns only read-only interfaces this entire interface IReadOnlyServiceProvider still can be considered as read-only. Possible implementation of this interface can look like this:

public class ServiceProvider : IReadOnlyServiceProvider
{
    // other code...
 
    public Dictionary<Typeobject> Services { getset; }
 
    public T GetService<T>()
    {
        object service;
        Services?.TryGetValue(typeof(T), out service);
        return service as T;
    }
}

As you can see here we have access to Services dictionary which allows us to modify output of GetService method as we want. Unfortunately we don't know exact types of content of this dictionary. In this case we can't avoid type casting during its modification:

var clone = readOnlyServiceProvider.Clone(p =>
{
    (sp.Services[typeof(IReadOnlyPhone)] as Phone).Number = "123-456-78";
});

In this case the rule of only one implementation of read-only interfaces is of great help again.

Methods in read-only interface


I have already talked about implementation of methods in read-only interfaces in the section devoted to collections. Implement methods using read-write delegate properties and pass instance of current object as an additional parameter to them:

public interface IReadOnlyPerson
{
    string FirstName { get; }
    string LastName { get; }
    string GetFullName();
    IReadOnlyPerson Clone(Action<Person> modifier = null);
}


public class Person : IReadOnlyPerson
{
    // other code...
    public Person()
    {
        FullNameProvider = (p) => $"{p.FirstName} {p.LastName}";
    }
 
    public string FirstName { getset; }
    public string LastName { getset; }
 
    public Func<Personstring> FullNameProvider { getset; }
 
    public string GetFullName()
    {
        return FullNameProvider?.Invoke(this);
    }
}

Conclusion


Let me finish this article by collecting all findings together:

  1. There must be only one implementation of each modifiable read-only interface.
  2. This implementation should allow modification of any its part:
    • If there are properties returning other read-only interfaces there should be read-write access to objects of classes implementing these interfaces.
    • If there are properties returning collections of read-only interfaces there should be read-write access to List<T> of objects where T implements these interfaces.
    • If there are methods returning read-only objects these methods should be implemented using delegates with read-write access to them. These delegates should get instance of current object as a parameter.
  3. You can implement initial cloning of objects using AutoMapper package.

No comments:

Post a Comment