Read-only interface is a simple thing. It does not allow the 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 whose state is almost the same as the old one.
Let's consider a 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 { get; set; }
public string LastName { get; set; }
}
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 the 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 a 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 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 a 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)
{
}
}
public class Data2 : IReadOnlyData
{
public Data2(ExternalData2 externalData)
{
}
}
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
{
}
public class DataFactory1
{
public IReadOnlyData CreateData(ExternalData1 externalData)
{
}
}
public class DataFactory2
{
public IReadOnlyData CreateData(ExternalData2 externalData)
{
}
}
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 an exact copy of existing object. We can certainly do it manually. But when number of properties of our objects grows, then also grows the 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<Person, Person>();
});
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 a 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 register themselves in the AutoMapper configuration. For this purpose, I created a 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<Person, Person>();
}
...
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<Person, Person>();
}
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
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<Address, Address>();
}
public string Country { get; set; }
public string City { get; set; }
}
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 the 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 a 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<Person, Person>();
}
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Phone> Phones { get; set; }
IEnumerable<IReadOnlyPhone> IReadOnlyPerson.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
{
public Func<Person, IEnumerable<IReadOnlyService>> ServicesProvider { get; set; }
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 IEnumerable
. It may look like Func<IEnumerabe>
would be sufficient. The problem is that AutoMapper can't clone delegates. It just leaves 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 the 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
{
public Dictionary<Type, object> Services { get; set; }
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
{
public Person()
{
FullNameProvider = (p) => $"{p.FirstName} {p.LastName}";
}
public string FirstName { get; set; }
public string LastName { get; set; }
public Func<Person, string> FullNameProvider { get; set; }
public string GetFullName()
{
return FullNameProvider?.Invoke(this);
}
}
Conclusion
Let me finish this article by collecting all findings together:
- There must be only one implementation of each modifiable read-only interface.
- This implementation should allow modification of any of its parts:
- 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.
- You can implement initial cloning of objects using AutoMapper package.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.