Внутренние установщики свойств в C#

Я пытаюсь найти хороший способ приблизиться к этому. У меня есть класс Customer, реализующий интерфейс ICustomer. Этот интерфейс имеет ряд свойств:

public interface ICustomer
{

   string FirstName {get; set;}
   string LastName  {get; set;}
}

Однако я хочу, чтобы определенные классы имели возможность устанавливать эти свойства; а именно те классы в проекте. Поэтому я подумал о том, чтобы сделать сеттер internal:

public class Customer : ICustomer
{

   string FirstName {get; internal set;}
   string LastName  {get; internal set;}
}

Однако я хотел бы пометить этот установщик как внутренний в интерфейсе, поэтому нет никаких шансов, что кто-то реализует ICustomer, а кто-то вне сборки изменит эти свойства. Есть ли хороший способ сделать это?


person larryq    schedule 28.11.2012    source источник


Ответы (2)


Свойства в интерфейсе должны быть доступны только для чтения. Допустимо, что конкретный класс, реализующий интерфейс, имеет установщик, даже если он не определен в интерфейсе.

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

public class Customer : ICustomer
{
   public string FirstName { get; internal set; }
   public string LastName  { get; internal set; }
}

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

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

internal interface IWriteCustomer
{
    string FirstName { set; }
    string LastName { set; }
}

internal interface IReadWriteCustomer : IReadCustomer, IWriteCustomer
{ }

public class Customer : IReadWriteCustomer
{
    private string _firstName;
    private string _lastName;

    public string FirstName
    {
        get { return _firstName; }
        internal set { _firstName = value; }
    }
    public string LastName
    {
        get { return _lastName; }
        internal set { _lastName = value; }
    }

    string IReadCustomer.FirstName
    {
        get { return FirstName; }
    }

    string IReadCustomer.LastName
    {
        get { return LastName; }
    }

    string IWriteCustomer.FirstName
    {
        set { FirstName = value; }
    }

    string IWriteCustomer.LastName
    {
        set { LastName = value; }
    }
}
person Servy    schedule 28.11.2012
comment
Спасибо за помощь. Однако, если я это сделаю, а класс-потребитель использует ICustomer, он не сможет получить доступ к установщику. - person larryq; 29.11.2012
comment
@larryq В этом весь смысл, потому что интерфейс не привязан к установщику. Если вы хотите реализовать второй внутренний интерфейс, который добавляет сеттер, вы можете это сделать. У меня часто есть ряд интерфейсов: это реальный пример: IDimensionedMap фиксирует только размеры 2D-объекта; IReadMap с доступными для чтения свойствами, IReadMap : IDimensionedMap только для чтения или вычисляемых карт, а затем IWriteMap : IReadMap. Но это сложное семейство расширяемых служебных классов. (Это двумерные гильбертовы пространства, а не словари, если вам интересно.) - person SAJ14SAJ; 29.11.2012
comment
@larryq Хотя я не уверен, нужно вам это или нет, я добавил альтернативную реализацию, в которой интерфейс определяет внутренний метод набора. - person Servy; 29.11.2012
comment
Спасибо, джентльмены, это отличные советы, и я высоко ценю их. - person larryq; 29.11.2012

Однако я хотел бы пометить этот установщик как внутренний в интерфейсе, поэтому нет никаких шансов, что кто-то реализует ICustomer, а кто-то вне сборки изменит эти свойства. Есть ли хороший способ сделать это?

Нет. К сожалению, члены свойства всегда общедоступны. Кроме того, возня с уровнями доступа к свойствам, часть которых указана в интерфейсе, становится болезненной, IIRC. Что вы можете сделать, так это:

public interface ICustomer
{
    string FirstName { get; }
    string SecondName { get; }
}

internal interface ICustomerWithSetMethods : ICustomer
{
    void SetFirstName(string name);
    void SetLastName(string name);
}

public class Customer : ICustomerWithSetMethods

Тогда снаружи это будет выглядеть так, будто Customer реализует только ICustomer, но внутри ваш код увидит, что он реализует ICustomerWithSetMethods.

К сожалению, это не очень хорошо работает, если вашему API нужно объявить какие-либо общедоступные методы, где вы действительно хотели бы просто объявить возвращаемый тип ICustomer, но на самом деле вы будете знать, что это всегда ICustomerWithSetMethods.

Предполагая, что вы все еще хотите разрешить несколько реализаций, вместо этого вы могли бы использовать абстрактный класс:

public abstract class CustomerBase
{
    public abstract string FirstName { get; }
    public abstract string LastName { get; }

    internal abstract void SetFirstName(string name);
    internal abstract void SetLastName(string name);
}

Теперь у нас есть небольшая странность, что никто за пределами сборки не может расширить ваш CustomerBase, потому что есть абстрактные методы, которые им придется переопределить, и которые они даже не могут видеть, но это означает, что вы можете используйте CustomerBase везде в вашем API.

Именно такой подход мы использовали в Noda Time для календарных систем в конце. Я записал об этом в блоге, когда впервые придумал план. Я обычно предпочитаю интерфейсы абстрактным классам, но здесь преимущество было значительным.

person Jon Skeet    schedule 28.11.2012
comment
Вы пропустили типы и область действия для FirstName и SecondName. - person Jon B; 29.11.2012
comment
@JonSkeet Я согласен с вашим подходом, но обычно я реализую как интерфейс, так и абстрактные базовые классы, когда ремонтопригодность с течением времени становится проблемой. Это требует дополнительного набора текста, но такие инструменты, как Resharper, упрощают задачу, а это означает, что вы можете иметь более одной абстрактной реализации, когда у вас есть сложное семейство объектов, возможно, такое, которое будет расширено за пределы вашей собственной библиотеки кода. - person SAJ14SAJ; 29.11.2012
comment
@ SAJ14SAJ: Дело не в дополнительном наборе текста — мне нравится иметь возможность накладывать интерфейс поверх этого; проблема в том, что вы не можете объявить внутренний член в интерфейсе. В этом суть этого вопроса. (В случае с Noda Time я действительно не возражаю против того, чтобы третьи стороны не могли добавлять новые реализации. Мы намеренно скрываем многие типы, которые должны быть в абстрактных подписях участников.) - person Jon Skeet; 29.11.2012
comment
@JonSkeet Извините, я пропустил эту тонкость. Я думал, что есть второй внутренний интерфейс, расширяющий первый общедоступный интерфейс, что является законным. Как правило, C# подходит ближе, чем любой другой классический объектный язык, к выражению того, что я хочу в этой парадигме! Мой компромисс для этих случаев также виден в некоторых классах библиотеки CLR: выбрасывание исключения IllegalOperationException для неподдерживаемой части интерфейса, предоставление свойств, которые позволяют сначала протестировать это, и документирование звукового сигнала из него. - person SAJ14SAJ; 29.11.2012
comment
@ SAJ14SAJ: Это не помогает, когда вам нужно ссылаться на внутренние типы в подписях членов, хотя, к сожалению ... Кажется, я попадаю в такие крайние случаи больше, чем другие! - person Jon Skeet; 29.11.2012
comment
Да, в C# нет понятия friend, как в C++, если я правильно помню 20 лет назад, когда я знал C++. Честно говоря, я думаю, что это, вероятно, правильное дизайнерское решение, но оно решило бы этот конкретный случай. - person SAJ14SAJ; 29.11.2012