Каков наилучший способ предоставить изменяемый интерфейс поверх неизменяемого?

#c# #inheritance #interface #properties #readonly

#c# #наследование #интерфейс #свойства #только для чтения

Вопрос:

Интересно, какова наилучшая практика в C # в отношении изменяемых / неизменяемых интерфейсов.

Мне нравится работать только с интерфейсами, а не с реальными объектами; удалите зависимости и упростите тестирование.

Я также обычно предоставляю интерфейс, доступный только для чтения, что снижает количество ошибок. однако это создает проблему в реальном коде, когда мне нужно что-то изменить в экземпляре объекта.

Вот что я пытаюсь сделать

 public interface ISomething
{
    string Name { get; }   
}

public interface IMutableSomething : ISomething
{
    string Name { get; set; }   
}

...

public class ConsumerClass
{
   //Note that I'm working against the interface, not the implementation
   public void DoSomethingOnName(ISomething o)
   {
       var mutableO = (IMutableSomething) o;
       mutableO.Name = "blah";
   }
}
  

Работа таким образом позволяет мне легко протестировать ConsumerClass и разорвать любую зависимость между ISomething и его реализацией

Я знаю, что мог бы применить интерфейс к реализации, но это привело бы к зависимости от реальной реализации.

Я мог бы сделать что-то вроде приведенного ниже, но я нахожу это уродливым и раздражающим

 public interface IMutableSomething : ISomething
{
    void SetName(string newName)   
}

or

public interface IMutableSomething // No inheritance, implementation impl. 2 interfaces
{
    string Name { get; set; }
}
  

Спасибо,

Эрик Дж.

Комментарии:

1. Мне это кажется немного забавным — вы передаете что-то с определенным интерфейсом (который я прочитал как «минимальное требование для корректной работы»), затем преобразуете это в объект, к которому предъявляются дополнительные требования (теперь должен быть разрешен set). Это вроде как установить указатель цели, а затем, когда кто-то достигает его, говорит: «О да, кстати, это не указатель цели, это просто маркер на полпути

2. Кроме того, интерфейсы ради интерфейсов на самом деле не так уж полезны.

Ответ №1:

Я думаю, что ваши интерфейсы прекрасны, но в вашем потребительском коде это должно выглядеть так:

 public class ConsumerClass{   
  // Just take IMutableSomething
  public void DoSomethingOnName(IMutableSomething o)   {       
    o.Name = "blah"; 
  }
}
  

Вызов метода — это контракт, и, как говорили другие, вам нужно указать наиболее общий тип, который вы ConsumerClass действительно можете использовать. Возможно, вы захотите ознакомиться с принципом подстановки Лискова: http://en.wikipedia.org/wiki/Liskov_substitution_principle .

В этом случае, хотя IMutableSomething может заменить ISomething , обратное неверно.

Комментарии:

1. То, что я пытаюсь сделать, это что-то вроде, var something = Repository.Get(...) что возвращало бы ISomething, а затем вызывало ConsumerClass.DoSomething(someThing) без необходимости приводить к IMutableSomething перед вызовом… Таким образом, я не могу случайно изменить имя

2. Это не лучшая идея. Если вам нужен IMutableSomething, у вас должно быть, чтобы ваш репозиторий мог возвращать что-то из IMutableSomething. То, что вы пытаетесь сделать, просто приведет к исключениям во время выполнения для ваших пользователей. Если вы не хотите случайно изменить ISomething, тогда используйте ISomethings везде, за исключением случая, когда вам действительно нужно изменить имя.

3. Спасибо за информацию. Это в основном то, что я пытаюсь сделать без необходимости приводить параметры перед вызовом.

Ответ №2:

На самом деле это не совсем правильное использование интерфейса; интерфейс устроен так, что вам все равно, какая реализация, вы просто используете определенные свойства и методы. Если вам нужно «установить» что-то, имеющее интерфейс только для получения, вы не должны передавать интерфейс в качестве параметра.

В такой ситуации, если вы ДОЛЖНЫ использовать интерфейс, определите метод set в своем интерфейсе (или реализуйте свойство по-другому)

 public interface ISomething
{
    string Name { get; set;}
    void SetName(string newValue);
}

// Choose one of these methods to implement; both is overkill
public class SomethingElse : ISomething
{
     protected string _internalThing = string.Empty;

     public string Name
     {
         get { return _internalThing; }
         set { throw new InvalidOperationException(); }
     }

     public void SetName(string newValue)
     {
         throw new InvalidOperationException();
     }
}
  

А затем просто попросите неизменяемые интерфейсы ничего не делать со значением (или выдавать исключение).

Комментарии:

1. Я не уверен, какова цель кода, который вы предлагаете; кажется, плохой дизайн — иметь установщик, который всегда выдает при вызове, только для того, чтобы иметь метод Set, который все равно выполняет настройку.