Click here to Skip to main content
15,891,896 members
Articles / Programming Languages / C#

Modifiable read-only Interface

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
16 Feb 2017CPOL6 min read 10.8K   2   2
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.

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:  

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

I have an object of this type:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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 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:

C#
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:

C#
Mapper.Initialize(cfg =>
{
    cfg.CreateMap<Person, Person>();
    // 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 a new interface:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

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

Collections in read-only Interface

Our read-only interfaces can provide access to collections:

C#
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:

C#
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:

C#
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:

C#
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<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:

C#
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:

C#
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:

C#
public class ServiceProvider : IReadOnlyServiceProvider
{
    // other code...
 
    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:

C#
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:

C#
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 { 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:

  1. There must be only one implementation of each modifiable read-only interface.
  2. 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.
  3. You can implement initial cloning of objects using AutoMapper package.
This article was originally posted at http://ivanyakimov.blogspot.com/feeds/posts/default

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finstek
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionmissing motivation Pin
Mr.PoorEnglish17-Feb-17 22:07
Mr.PoorEnglish17-Feb-17 22:07 
AnswerRe: missing motivation Pin
Ivan Yakimov19-Feb-17 21:57
professionalIvan Yakimov19-Feb-17 21:57 
Thank you for your comment! You are right, I have not discussed applicability of this method properly. I came across this problem in one of my projects. The problem seemed interesting for me and I decided to share my thoughts about it. In short, there was a read-only interface providing visualization options for some data. The interface was very complex and process of its building was complex as well. But in some places I needed to slightly change these visualization options.

I understand that my solution is not the only one. For example I could make my visualization options interface writable. But it would make my program more complex to analyze and debug. So I didn't want to go that way. Another approach could be to separate visualization options to read-only and changable parts. As I have said, my solution is only one of many possible approaches.

You are asking: "Whats the use of an Interface, which only can be implemented once?". In this problem I consider interface as a contract. It promise me two things:
* provides me with some data
* promises me that this data will not be changed
These two things are more important for me than ability to implement the interface in different ways. They simplify analysis of my application (e.g. I can be sure I can use it in separate threads).

On the other hand I still can create different implementations of the interface in my tests, because there I don't actually need modifiability.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.