Пользователь
0,0
рейтинг
18 сентября 2010 в 05:52

Разработка → Singleton (Одиночка) или статический класс?

Статья будет полезна в первую очередь разработчикам, которые теряются на собеседованиях когда слышат вопрос «Назовите основные отличия синглтона от статического класса, и когда следует использовать один, а когда другой?». И безусловно будет полезна для тех разработчиков, которые при слове «паттерн» впадают в уныние или просят прекратить выражаться :)

Что такое статический класс?


Для начала вспомним что такое статический класс и для чего он нужен. В любом CLI-совместимом языке используется следующая парадигма инкапсуляции глобальных переменных: глобальных перменных нет. Все члены, в том числе и статические, могут быть объявлены только в рамках какого-либо класса, а сами классы могут (но не должны) быть сгруппированы в каком-либо пространстве имен. И если раньше приходилось иммитировать поведение статического класса с помощью закрытого конструктора, то в .NET Framework 2.0 была добавлена поддержка статических классов на уровне платформы. Основное отличие статического класса от обычного, нестатического, в том, что невозможно создать экземпляр этого класса с помощью оператора new. Статические классы по сути являются некой разновидностью простанства имен — только в отличие от последних предназначены для размещения статических переменных и методов а не типов.


Что такое Singleton (Одиночка)?


Один из порождающих паттернов, впервые описанный «бандой четырех» (GoF). Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Мы не будем подробно рассматривать здесь этот паттерн, его предназначение и решаемые им задачи — в сети существует масса подробной информации о нем (например здесь и здесь). Отмечу лишь что синглтоны бывают потокобезопасные и нет, с простой и отложенной инициализацией.

А если нет разницы — зачем плодить больше?


Так в чем же все-таки разница между этими двумя сущностями и когда следует их использовать? Думаю что лучше всего это проиллюстрировать в следующей таблице:
Singleton
Static class
Количество точек доступа
Одна (и только одна) точка доступа — статическое поле Instance
N (зависит от количества публичных членов класса и методов)
Наследование классов
Возможно, но не всегда (об этом — ниже)
Невозможно — статические классы не могут быть экземплярными, поскольку нельзя создавать экземпляры объекты статических классов
Наследование интерфейсов
Возможно, безо всяких ограничений
Невозможно по той же причине, по которой невозможно наследование классов
Возможность передачи в качестве параметров
Возможно, поскольку Singleton предоставляет реальный объект
Отсутствует
Контроль времени жизни объекта
Возможно — например, отложенная инициализация (или создание по требованию)
Невозможно по той же причине, по которой невозможно наследование классов
Использование абстрактной фабрики для создания экземпляра класса
Возможно
Невозможно по причине осутствия самой возможности создания экземпляра
Сериализация
Возможно
Неприменима по причине отсутствия экземпляра

Рассмотрим подробнее перечисленные выше критерии.

Количество точек доступа

Конечно же имеются ввиду внешние точки доступа, другими словами — публичный контракт взаимодействия класса и его клиентов. Это удобнее проиллюстрировать с помощью кода:

Singleton в «канонической» реализации:
public class Session
{
    private static Session _instance;

    // Реализация паттерна ...

    public static Session Instance
    {
        get
        {
            // ...
            return _instance;
        }
    }

    public IUser GetUser()
    {
        // ...
    }

    public bool IsSessionExpired()
    {
        // ...
    }

    public Guid SessionID
    {
        get
        {
            // ...
        }
    }
}


Статический класс:
public static class Session
{
    // Точка доступа 1
    public static IUser GetUser()
    {
        // ...
    }

    // Точка доступа 2
    public static bool IsSessionExpired()
    {
        // ...
    }

    // ...

    // Точка доступа N
    public static Guid SessionID
    {
        get
        {
            // ...
        }
    }
}


Наследование классов

С наследованием статических классов все просто — оно просто не поддерживается на уровне языка. С Singleton все несколько сложнее. Для удобства использования многие разработчики чаще всего используют следующую реализацию паттерна:
public class Singleton<T> where T : class
{
    private static T _instance;

    protected Singleton()
    {
    }
        
    private static T CreateInstance()
    {            
        ConstructorInfo cInfo = typeof(T).GetConstructor(
            BindingFlags.Instance | BindingFlags.NonPublic,
            null,
            new Type[0],
            new ParameterModifier[0]);

        return (T)cInfo.Invoke(null);
    }

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = CreateInstance();
            }

            return _instance;
        }
    }
}

public class Session : Singleton<Session>
{
    public IUser GetUser()
    {
        // ...
    }

    public bool IsSessionExpired()
    {
        // ...
    }

    public Guid SessionID
    {
        get
        {
            // ...
        }
    }
}

А поскольку множественное наследование в C# и в любом CLI-совместимом языке запрещено — это означает что мы не сможем унаследовать класс Session от любого другого полезного класса. Выходом является делагирование синглтону управления доступом к экземпляру объекта:
public class Session : CoreObject
{
    private Session()
    {
    }

    public static Session Instance
    {
        get
        {
            return Singleton<Session>.Instance;
        }             
    }
}

Наследование интерфейсов

Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное — избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
// Этот класс является синглтоном и реализует интерфейс ISession
public class Session: CoreObject, ISession
{
    private Session()
    {
    }

    public static Session Instance
    {
        get
        {
            return Singleton<Session>.Instance;
        }             
    }
}

// Этот класс не является синглтоном и вообще может быть объявлен и реализован в другой сборке
// полностью скрывая детали реализации
public class VpnSession : ISession
{

}

public interface ISessionManager
{
    ISession GetSession(Guid sessionID);

    // Принимает интерфейс ISession, следуя принципам уменьшения связности
    bool IsSessionExpired(ISession session);        
}


Возможность передачи в качестве параметров

Для статических классов это не поддерживается — можно передать разве что тип, но в большинстве ситуаций это бесполезно, за исключением случаев применения механизмов отражения (reflection). Синглтон же по сути является обычным экземпляром объекта:

// ...
ISessionManager _sessionManager;

// ...
bool isExpired = _sessionManager.IsSessionExpired(Session.Instance);

Контроль времени жизни объекта

Время жизни статического класса ограничено временем жизни домена — если мы создали этот домен вручную, то мы косвенно управляем временем жизни всех его статических типов. Временем жизни синглтона мы можем управлять по нашему желанию. Яркий пример — отложенная инициализация:
public class Singleton<T> where T : class
{
    // ...

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
               // Создание "по требованию"
                _instance = CreateInstance();
            }

            return _instance;
        }
    }
}

Можно также добавить операцию удаления экземпляра синглтона:
public class Singleton<T> where T : class
{
    // ...

    public static T Instance
    {
       // ...
    }

    // Очень опасная операция!
    public void RemoveInstance()
    {
        _instance = null;
    }
}

Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования — например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
public class Singleton<T> where T : class
{
    // ...

    public static T Instance
    {
        get
        {
            if (!IsAlive)
            {
               // Удаление по условию
                RemoveInstance();
            }
            if (_instance == null)
            {
               // Создание "по требованию"
                _instance = CreateInstance();
            }

            return _instance;
        }
    }

    private void RemoveInstance()
    {
        _instance = null;
    }
}


Использование абстрактной фабрики для создания экземпляра класса

Статический класс не поддерживает данной возможности ввиду того, что нельзя создать экземпляр статического класса. В случае с синглтоном все выглядит просто:
public interface IAbstractFactory
{        
    T Create<T>();

    bool IsSupported<T>();
}

public class Singleton<T> where T : class
{
    private static T _instance;

    private static IAbstractFactory _factory;

    protected Singleton(IAbstractFactory factory)
    {
        _factory = factory;
    }

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = _factory.Create<T>();
            }

            return _instance;
        }
    }
}

// Вариант с прямым наследованием от синглтона
public class Session : Singleton<Session>
{
    protected Session()
        : base(new ConcreteFactory())
    {
    }

    // ...
}

Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
public class Session : CoreObject, ISession
{
    private class SessionSingleton : Singleton<Session>
    {
        protected SessionSingleton()
            : base(new ConcreteFactory2())
        {
        }
    }

    private Session()
        : base(new CoreContext())
    {
    }

    public static Session Instance
    {
        get
        {
            return SessionSingleton.Instance;
        }             
    }

    // ...
}


Сериализация

Сериализация применима только к экземплярам классов. Статический класс не может иметь экзмпляров поэтому сериализовать в данном случае нечего.

Так что же использовать Синглтон или Статический класс?


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

Использование синглотона оправдано, когда:
  • Необходимо наследование классов или интерфейсов или делегаровать конструирование объектов фабрике
  • Необходимо использование экземпляров класса
  • Необходимо контролировать время жизни объекта (хоть это и очень редкая задача для синглтона)
  • Необходимо сериализовать объект (такая задача гипотетически возможна, но трудно представить себе сценарии использования)

Использование статических классов целесообразно тогда, когда у вас нет необходимости реализовывать ни один из сценариев перечисленных для синглтона. Основное назначение статических классов все-таки в группировке логически схожих методов, констант, полей и свойств. Например: System.Math, System.BitConverter, System.Buffer, System.Convert и т.д.
Олег Стельмахов @DangerT
карма
29,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (84)

  • +16
    Я так понимаю что объяснение — «Статичный класс не имеет инстансов, а Singleton имеет только один», слишком банальное?
    • 0
      Я хотел раскрыть данный вопрос именно в ньюансах некоторых сценарияев использования.
      • +14
        В ньюансах? Думаю стоило в разрезе олдансов тоже посмотреть… Жесть… Бррррх…
  • +7
    Сингелтон — это логическая бомба, его использование почти всегда неоправданно. Основной его минус состоит в том, что он активно мешает масштабированию системы.
    • +2
      Согласен. Но иногда некоторые объекты очень удобно делать синглтонами если это не вступает в конфликт с общей архитектурой системы. Ну напрмер некий менеджер сессий SessionManager, репозитарий словарей DictionaryRepository и т.д.
      • 0
        Ну а почему не может быть несколько менеджеров сессий или репозитариев словарей? зачем здесь singleton?
        • 0
          Но иногда некоторые объекты очень удобно делать синглтонами

          Никто не говорил что не может быть нескольких экземпляров вышеперечисленных объектов :)
          • 0
            Ну — значит пример неудачный.
            вот этот пример мне показался более удачным: habrahabr.ru/blogs/refactoring/103681/#comment_3262041
            • 0
              Да, пример хороший. Очень коррелирует с текстом, конкретно с разделом «Количество точек доступа»
    • 0
      Синглтон — это чудесное спасение :) Основные его плюсы начинают проявляться только после того, как вы вымарываете из интерфейса объекта все намеки на то, что это именно синглтон, а не обычный объект.
      • 0
        Синглтон — это чудесное спасение :)

        Статья выглядит именно так? Самое интересно что я такой цели не преследовал :)
      • 0
        Кстати а что вы имели ввиду под «инрерфейсом»?
      • 0
        Это сарказм? Я не понял, что вы хотите сказать, переформулируйте.
        • 0
          Хм, видимо действительно непонятно получилось, попробую подробнее, и на всякий случай без привязки к дотнету, как это получилось у автора.
          Мы говорим о синглтоне, как об объекте, у которого существует только один экземпляр. Пока это так, есть куча мест, где синглтон может быть выгоден — например, у вас есть объект, который бессмысленно инициализировать каждый раз при вызове, т.к. он все равно не меняется в заданных условиях (объект с разобранными данными http-запроса, объект с данными конфигурационного файла и т.д.). Те, кто не читали о том, что глобальные переменные — зло, запихивают такого рода данные в глобальные переменные, и проигрывают, как только начальные условия меняются (например, в зависимости от имени хоста должны подгружаться разные конфиги). Проигрывают, как правило, и те, кто сделал для объекта «классическую» точку доступа в виде метода getInstance — она теряет свой смысл и сбивает с толку, в случае если объект понадобится перестроить из классического синглтона во что-то другое. Вывод лично у меня простой: о том, что объект работает как синглтон, не должен знать никто снаружи (используем самописный new вместо getInstance). Это помогает как в случаях, когда «традиционный» объект нужно превратить в синглтон, так и в обратных. Проблем при рефакторинге за несколько лет тоже не замечено.
          Сорри если опять сумбурно получилось :)
          • 0
            (используем самописный new вместо getInstance)

            Переопределить оператор new вы сможете в C++. А как же быть с С#? Я не говорю о других, более примитивных CLI-совместимых языках — например VB .NET. Да и вообще сложно себе представить как вы будете с помощью переопределения new реализовывать синглотон. Слушайте — правда очень заинтересовало. Если не сложно — можно примеры кода? Если не можете опубликовать здесь — в личку, я никому их не покажу, честно :)
            • 0
              Я специально написал дисклеймер про непривязку к дотнету, т.к. не был уверен, что в шарпе и иже с ним можно баловаться подобным образом. Если нельзя — значит нельзя. В том же перле такие вещи делаются на раз, а сам объект можно хранить в переменной класса.
              my $self;
              sub new {
                  my $class = shift;
                  unless ($self) {
                      $self = $class->SUPER::new;
                      # some init code
                  }
                  return $self;
              }

              Таким образом, все нюансы, связанные с единственностью объекта, остаются в самом объекте. При необходимости аналогичным образом можно прозрачно сделать пул и прочие интересности, но наружу ничего не выплывет.
    • +4
      Вне всяких сомнений, синглетоны *с глобальной точкой доступа* используют только лохи. Их удобство обманчиво, ведь в большинстве случаев получается та же самая глобальная переменная, а если их (таких синглетонов) становится много (а такое бывает нередко, синглетоны как чипсы: съел один, и тут же хочется второй), то значительно затрудняется рефакторинг. Гораздо разумнее использовать концепцию обращения контроля и её частный случай — внедрение зависимости. Необходимые синглетоны при этом никуда не деваются, но к ним уже нельзя обратиться откуда попало, контейнер сам следит за тем, чтобы разные объекты при создании получали один и тот же экземпляр синглетона, а весь граф взаимодействия объектов в приложении формируется, как правило, в самом начале работы программы.
      • 0
        Я придерживаюсь такой же точки зрения, и, высказываясь про сингелтоны, имел ввиду именно ту реализацию, что в статье.
    • +1
      Почти… но один два синглтона на всё приложение почти всегда оправдано :)
      • +1
        В некоторых случаях вместо синглтона удобнее использовать контекст выполнения.
        • 0
          Да — видел реализацию этого принципа на практике. Очень понравилось — даже срочно внедрили в проект. Пока только в одной изолированной сборке к сожалению.
  • +1
    Кроме есть замечания по коду:

    public class Singleton<T> where T : class
    {
        private static T _instance;
            
        private static T CreateInstance()
        {            
            ConstructorInfo cInfo = typeof(T).GetConstructor(
                BindingFlags.Instance | BindingFlags.NonPublic,
                null,
                new Type[0],
                new ParameterModifier[0]);
    
            return (T)cInfo.Invoke(null);
        }


    нужно заменить на

    public class Singleton<T> where T : class, new()
    {
        private static T _instance;
    
        protected Singleton()
        {
        }
            
        private static T CreateInstance()
        {            
            return new T();
        }


    Ну и рабока с многопоточностью не рассмотрена.
    • +2
      Вы видимо недопоняли зачем нужен код, использующий поиск конструктора. Предложенный вами вариант будет работать только если класс-синглтон объявлен так:

      public class Session : Singleton<Session>
      {
          public Session()
          {
          }
       
          public bool IsSessionExpired()
          {
              return false;
          }
      }


      В противном случае вы получите на этапе компиляции 'ConsoleApplication1.Session' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'ConsoleApplication1.Singleton'.
      Ну а поскольку вы будете вынуждены сделать конструктор открытым — возможно будет создать много экземпляров этого класса. Какой же это синглтон?
      • +1
        Согласен, невнимателен =)
    • 0
      А работу с многопоточносю я НАМЕРЕННО не стал рассматривать :) Так как, во-первых очень спать хотелось, а во вторых, думаю все вменяемые разработчики уже давно используют Double-Checker для синхронизации доступа к экземпляру синглотона. Например, так:
      public class Singleton<T> where T : class
      {
          // ...
       
          public static T Instance
          {
              get
              {
                  if (_instance == null)
                  {
                      lock (_locker)
                      {
                          if (_instance == null)
                          {
                              _instance = CreateInstance();
                          }
                      }
                  }
       
                  return _instance;
              }
          }
      }
      • +2
        а вот про double-checker в последнее время отзываются не очень хорошо:
        www.yoda.arachsys.com/csharp/singleton.html
        • 0
          Спасибо за интересную информацию! С интересом прочел.
        • 0
          Честно говоря не задумывался ранее о совместимости с Java :)
      • +2
        В .NET 4 можно не запариваться с базовыми классами для singlton, а писать так
        public sealed class Session
        {
        private static readonly Lazy InstanceField = new Lazy(() => new Session());

        private Session()
        {

        }

        public static Session Instance
        {
        get
        {
        return InstanceField.Value;
        }
        }

        // ...
        }

        Кода немного, ничего страшного, зато все безопасно и работает.
        • +1
          Совершенно верно. Хотел привести в конце этот вариант, но не стал. Хотелось написать более «академическую» статью без деталей имплементации .NET 4.0.
    • +1
      Кстати, вместо длинного вызова GetConstructor + Invoke можно было использовать Activator.CreateInstance(typeof(T))
  • 0
    Спасибо за статью. Правда не всегда вопрос поставлен именно так — или синглтон, или статический класс. Не всегда целесообразно логику объекта и логику создания (поиска) объекта помещать в одном месте (синглтон). Если злоупотреблять синглтонами, то получим много дублирующегося кода и как следствие — трудности с расширением архитектуры. В таких ситуациях спасают, например, фабричные методы.
  • 0
    Пишу на дельфи, поэтому не совсем понимаю откуда взялось ограничение на наследование. Это в дотнете так сделано, или имеется в виду что-то концептуальное?
    • 0
      Это ограничения любого CLI-совместимого языка платформы .NET — С#, VB .NET и пр.
      • 0
        Не думаю чтобы из нативного кода тут можно было добиться большего, чем на дотнете. Можно же создать класс, технически не являющийся статическим, но имеющий только члены класса, среди которых и виртуальные методы?
        • 0
          Можно — но никто не мешает расширить его функционал в производных классах.
    • 0
      Самое забавное, что это хорошее ограничение.
      Оно заставляет хорошо продумывать правильную архитектуру и использовать интерфейсы вместо базовых классов.

      Composition over Inveritance ;)
      • 0
        Наследование — не нужно? :)
        • +2
          Я не говорю, что не нужно. Я хочу сказать, что отсутствие множественного наследования классов в C# ничуть не мешает. Можно наследовать множество интерфейсов — и это используется везде и по максимуму.

          И на мой взгляд лучше не плодить иерархию классов, это делает компоненты кода слишком связанными. Другое дело интерфейсы — это всего лишь контракты и одну реализацию всегда можно «незаметно» заменить другой.
          • 0
            Нет, я не о множественном наследовании (его в дельфи тоже нет). Насколько я понял из топика, вообще нельзя наследовать от статического класса.
            • 0
              А, ну это да.

              Наследование в .NET определено только для чего-то «экземплярного», т.е., например, классов. И методы статических классов определяются только на уровне типов, но не экземпляров.
              • 0
                Все-таки недопоняли. В дельфи есть в три типа методов — методы экземпляра, методы класса, и статические методы класса.
                Вторые вполне могут быть виртуальными, а вызываются на классе, а не экземпляре. Поэтому имеет смысл наследование класса, экземпляр которого никогда не создаётся.
                • 0
                  Не совсем понял ваше объяснение. Причем тут виртуальные методы? Они только определяют, как «смотреть» на объект в конкретном контексте — как на экземпляр базового класса или как на экземпляр дочернего класса, чтобы определить чей метод вызвать (если он переопределен, конечно). Но вызывается то он у экземпляра, а не класса. Или в Дельфи какая-то своя специфика? (в чем я сомневаюсь, ибо это концептуальная вещь).

                  Если уже лезть в дебри реализации на C# — там неявно при вызове метода у объекта (экземпляра), первым параметром в этот метод передается указатель на текущий экземпляр класса. Но это происходит только для нестатических методов классов, ибо у статических нет экземпляров, и компилятор не сможет определить какой именно метод выполнить — базового статического класса, или дочернего.

                  P.S. К слову, не вижу ни одного сценария, где нужно наследование статиков. Внутреннее ощущение, что это вообще противоречит концепциям ООП.
                  • 0
                    Виртуальные методы тут при том, что с ними в наследовании больше смысла.
                    А вызываются они именно что не у экземпляра, а у класса, я об этом и толкую.

                    Не вижу что тут противоречит ООП
                    • +1
                      Сначала про ООП — я не вижу смысла в static virtual, когда можно использовать для этого просто virtual. Концептуально — предположим, что static class и есть сам по себе экземпляр чего-то (например, Планеты Земля). Тогда наследующий его класс (тоже статический) будет в том числе экземпляром того-же самого (Планеты Земля). И эти два экземпляра планеты Земля должны будут сосуществовать в одной вселенной (одном Application Domain). Как? :)

                      «А вызываются они именно что не у экземпляра, а у класса»
                      Можно поподробнее?
                      • 0
                        Ну, примерчик тот еще. Окей, это может пригодиться если вы решите расширить модель, чтобы она включала М-теорию.

                        Вместо подробнее вот вам пастебин pastebin.com/RxnQXKst
                        • 0
                          Да, модель с M-теорией будет использоваться в одном контексте, а модель без нее — в другом. Но все равно не будет ситуации, когда в одном контексте, одной области видимости 2 статика одного типа. Если же нужно, чтобы это было возможно — нельзя (и реально нет необходимости) делать их статиками. Если в одном AppDomain меняется сам контекст (нужна то более сложная модель, то более простая), и нужна замена — никаких статиков, в крайнем случае — Singleton.
                          • 0
                            В М-теории предполагается сразу куча контекстов, и в каждом по Земле
                            • 0
                              Статики тут тогда тем более ни при чем. Вообще. Куча контекстов — речь об экземплярах, экземплярах, экземплярах.

                              К слову, то что я пытаюсь донести, и что хотел сказать фразой про противоречие концепциям ООП, отлично выразил ниже в комментарии товарищ bobermaniac. Статики — это не часть ООП, и я с этим согласен.
                              • 0
                                Ну если так глобально — то да. Я имел в виду что наследование без экземпляров нарушает его не более чем одни только статики.
                          • 0
                            И собсно роль «синглтона» играет каждый sealed-потомок, а предка вообще не обязательно рассматривать в этом смысле, он просто реализовывает общую базу
                            • 0
                              Именно, «синглтона». И статики тут ни при чем.
                        • +1
                          Кстати, я посмотрел код по вашей ссылке. По сути, на C# такое можно сделать (т.е. вызвать Class.Method() без создания объекта), если использовать статические методы. Если класс имеет статик-методы, но сам при этом не статик, его, кстати, можно унаследовать.

                          Обратите внимание, ваш метод независим от класса, он не оперирует его состоянием и внутренними переменными, его задача просто вывести фразу или сделать еще какую-то общую вспомогательную вещь. Кстати, поэтому в C# статики часто используются для всевозможных Helper-ов, чтобы просто логически объединить некоторый функционал, не более.
                          • 0
                            Ну это будет не совсем то. Дело в том, что при наследовании, если вы захотите переопределить в другом классе некий статический метод или свойство — вам придеться воспользоваться модификатором new. А это синтаксически будет совсем не то же самое что перегрузка.
                            • 0
                              Собственно, да. Это вообще две разные конструкции. Но это неважно, все равно сходу не вижу сценариев использования приведенного кода :)
                          • 0
                            Это просто простой пример. Если я объявлю поля как class var, то мой метод сможет оперировать состоянием, при этом экземпляр не требуется
                            • 0
                              А состоянием чего тогда будет оперировать метод?
                              Я себе могу представить только то, что неявно создается какой-то «анонимный объект по-умолчанию» или что-то в этом роде.

                              P.S. Ухх, представляю каково все это тестировать :)
                              • 0
                                Состояние класса. Если рассматривать данные класса, то никаких сложностей с ними нет — это просто статические переменные, с ограниченной областью видимости. Это нельзя назвать экземпляром поумолчанию, потому что у класса и экземпляра разный набор полей. Поля класса находятся прямо в секции данных бинарника, и никаких отдельных усилий для создания прилагать не нужно. Но есть конструкторы классов, выполняющиеся автоматически при запуске. Но они вроде и сишарпе есть, так что тут сравнивать нечего
                • 0
                  Черт… Совершенно забыл за несколько лет забвения Делфи… Что такое «методы класса» — пример краткий можно?
          • +1
            В моей практике уже есть как минимум два сценария, в который множественное наследование было бы КРАЙНЕ желательным. И вы не поверите, к каким ухищрениям пришлось прибегнуть, чтобы это обойти.
            • 0
              Не исключено. Но всегда есть обходной путь. Можно примерчик сценария?
              • 0
                Код приводить не могу — NDA. Скажу вкратце — нужнь было бизнес-объект включить в дерево в пользовательском интерфейсе. Конечно проникновение UI в бизнес-слой — это грубейшая ошибка проектирования, но так надо было. Такая специфическая задача была — обертку невозможно было использовть. Да еще и для организации взаимодействия с UI использовался MVVM. Так вот существовал класс, реализующий полностью функционал узла дерева — TreeNodeBase. И был некий LinqEntityBase класс. Задача изящно решалась бы если было бы возможно: Product: LinqEntityBase, TreeNodeBase. А на деле пришлось: Product: LinqEntityBase, ITreeNode + паттерн Декоратор.
                • 0
                  Но это действительно очень специфичный сценарий, имеющий множество ограничений изначально:
                  — «проникновение UI в бизнес-слой» и «так надо было»
                  — «некий LinqEntityBase»

                  Первое — действительно грубейшая ошибка. Product не должен знать, что он является TreeNode'ом. А то так то он может быть еще и MenuItem'ом, и SomethingElse'ом. Единственный логичнейший выход — Продукт реализовывает ITreeNode, к тому же он наверняка реализовывал какие-то свои особенности «встраивания». Ну и само собой, про ITreeNode должен бы знать только ProductViewModel, но никак не Product.
                  Этот приведенный пример (вынуждающие условия) сам по себе ведет к «решению на костылях», и главным костылем тут было бы использование множественного наследования.

                  Второе — правильно ли я понимаю, что использовался EF1? С точки зрения хорошей архитектуры (желательно Domain-Driven), сущности предметной области должны быть POCO-классами, не используя общий какой-то контекст из родительского класса, так что это скорее всего издержки технологии.
                  • 0
                    Нет — не EntityFramework. Стандартный SqlMetal для процессинга Sql to Linq + своя кастомная кодогенерация на основе T4 (у нас объекты генерятся по схеме БД) для навешивания аттрибутов валидации и интеграции связки с бизнес-логикой.
                  • 0
                    А от связности UI и слоя данных я уже практически ушел.
                • 0
                  как вариант использовать generic:
                  class TreeNodeBase where TEntity: LinqEntityBase

                  class Product: LinqEntityBase

                  class ProductTreeNode: TreeNodeBase
                  • 0
                    как всегда пропали скобки

                    class ProductTreeNode: TreeNodeBase[Product]
                    • 0
                      Был такой вариант. Категорически не подходит для биндинга, поскольку используется паттерн MVVM для организации UI.
      • 0
        Чем по сути отличается наследование от абстрактного класса и реализация интерфейса? Парой ключевых слов? В тех языках, конечно, где разрешено множественное наследование.
        • 0
          Сложностями с полями. Ну или их отсутствием
        • +1
          Ну, скажем так, концептуально интерфейсы предоставляют большую гибкость — они определяют обязательный контракт с «внешним миром», который реализующие его классы должны выполнить.

          И за счет жестко установленных контрактов мы получаем уверенность, что конкретный объект реализует интерфейс и может рассматриваться в соответствующей ситуации именно как экземпляр этого интерфейса. Все это позволяет развязать компоненты между собой (они не связаны иерархией наследования). Принципы SOLID — это «the must» в проектировании.

          Однако я не сказал бы, что абстрактные классы вообще не нужны, ровно как и наследование от них. Просто на практике, при построении «правильной» архитектуры, с огромным отрывом превалируют именно интерфейсы. Но бывают ситуации, когда объекты имеют достаточное количество общей логики, чтобы ее было логично вынести в абстрактный базовый класс и использовать как поведение по-умолчанию. Все же, такие ситуации бывают редко, что является следствием использования SOLID.
          • 0
            По-моему мы о разном говорим. Вы говорите об интерфейсах как о логической сущности или, если угодно, термине проектирования, я же о конкретной синтаксической единице ЯП. Какая разница иерархия наследования от иерархии абстрактных классов или иерархия имплементации иерархии интерфейсов? :) Главное, чтобы был «обязательный контракт с «внешним миром», который реализующие его классы должны выполнить.»

            Если вы, например, переносите класс в другой проект, то вам надо будет и переносить либо все классы от которых он унаследован, либо все интерфейсы, связанность и тут, и тут. То же самое, если решите добавить метод в «контракт», в любом случае реализацию придётся модифицировать. И по вашему получается, что на таких языках как python, где интерфейсов, как части языка, нет в принципе невозможно спроектировать приложение согласно SOLID? По-моему, возможно :) Более того, в языках с утиной типизацией даже наследование от абстрактных классов(имплементации интерфейсов) формально не нужно, достаточно чтобы объект (сознательно не пишу класс) реализовывал нужные сигнатуры методов. Ну а если не реализует, то получим исключение при попытке такие методы вызывать (в статических языках получим ошибку компиляции). Хотя лично я предпочитаю создавать класс, назвать его IRepository :) и наследоваться от него, при необходимости добавить в него какую-то общую для всех репозиториев логику или свойства максимумм что мне нужно, так переименовать этот класс в RepositoryAbstract или RepositoryBase.

            По-моему, интерфейсы, как часть языка, являются лишь ещё более абстрактными классами, чем собственно абстрактные классы. Просто соглашение между кодерами — если в абстрактном классе нет кода, кроме объявления абстрактных методов, можно сделать его интерфейсом (а можно и не делать, ни на что по сути это не влияет). В языках же где множественного наследования нет, разработчиков вынуждают те классы, которые могли бы быть просто абстрактными объявлять интерфейсами и общую логику реализовывать либо непосредственно в классах, реализующих их, либо вводить абстрактный класс, частично реализующий интерфейс. Не вижу никакого уменьшения связанности. По сути — интерфейсы в языках с множественным наследованием (как и «обычные» абстрактные классы) лишь «фулл протекшен», так же и в языках без множественного наследования, где от него отказались в пользу интерфейсов сознательно, а не из-за необходимости облегчить работу транслятору :) Там же где нет множественного наследования «по техническим причинам» интерфейсы лишь костыль, позволяющий его хоть как-то реализовать.
            • 0
              С последней мыслью не согласен в корне — интерфейсы — это отдельная концепция и она именно концептуально и отличается от абстрактных классов.

              С точки же зрения синтаксиса языка, наверное, действительно, разницы особой нет — в C#, например, это просто отдельный механизм именно для поддержания этой концепции, и интерфейсы используются, чтобы проверять факт реализации сигнатуры интерфейса при компиляции и корректность реализации тестами.

              Про «интерфейсы в языках с множественным наследованием» — тут да, по сути, можно сделать то же самое двумя способами. Но лучше, имхо, объявлять интерфейсы именно как интерфейсы, это что-то вроде соглашения (для уверенности, что контракт будет выполнен).

              Здесь кроется важная мысль — в таких языках абстрактные классы можно использовать как интерфейсы, а можно — как абстрактные базовые классы.

              И тут уже просто приходим к разным подходам к проектированию — Inheritance или Composition.

              Однако стоит сказать еще вот про какую вещь — «если решите добавить метод в «контракт», в любом случае реализацию придётся модифицировать».
              В большинстве случаев возникновение такой ситуации — это нарушение Open-Closed Principle и/или Single Responsibility Principle, и зачастую для новой функциональности лучше создать отдельный интерфейс, реализации которого и будут содержать новую логику (включая наш новый класс, который мы иначе унаследовали бы от базового абстрактного). То есть, по сути, я лучше создам новый контракт и соберу конечный класс из множества кусочков-контрактов. Но это уже опять о подходах к проектированию, а не инструкциях языка. Повторюсь — можно _синтаксически_ использовать и abstract class, но если есть interface — то зачем?

              P.S. Пора писать статью со своим взглядом и аргументами, почему Composition лучше Inheritance :)
              • 0
                По-моему, в случае добавления метода нарушения Open-Closed Principle не будет, ведь мы не модифицируем существующий код, а расширяем его. Расширять можно по разному, главное не затрагивать уже существующие методы. На каждый метод создавать отдельный интерфейс/класс как-то не тру. Или я вообще Open/Closed не понял. Он что не совместим с Test Driven Development, где разработка в принципе и заключается в добавлении новых методов/свойств/классов по мере необходимости реализации требований ТЗ и постоянном рефакторинге (включая и изменение интерфейсов классов)?

                Для публичного продукта, предназначенного для других разработчиков, фиксировать внешние интерфейсы конечно необходимо и добавлять новую функциональность, не ломая старую является, как минимум, хорошим тоном (хотя бы в минорных релизах). Но фиксировать внутренние интерфейсы после каждого удачного прогона, а после этого изменять их только наследованием или созданием новых интерфейсов — перебор, по-моему, так как код станет абсолютно нечитаемым и неуправляемым, логика просто утонет за кучей оберток к каждому методу, а то и к каждой сигнатуре метода.

                P.S. Под противостоянием композиции и наследования как-то всегда понимал что-то вроде (псевдо Си, нормального С++ не помню уже :) ):
                class A:B {
                B _b;

                A() {
                _b = new B();
                }

                getB() {
                return _b;
                }
                ...
                }

                class B {

                void doSmth () {
                ...
                }
                ...
                }

                A.getB().doSmth();

                против
                class A:B {
                ...
                }

                class B() {
                void doSmth () {
                ...
                }
                ...
                }

                A.doSmth();

                и причём тут интерфейсы не понял :-/ Так что пишите статью ;)
  • +1
    >Необходимо сериализовать объект (такая задача гипотетически возможна, но трудно представить себе сценарии использования)

    По-моему, как раз самый распространённый вариант использования синглтонов — хранение настроек приложения, при входе в приложение десериализуем его из конфигов, при изменении — сериализуем, а так всё приложение имеет доступ к одному и тому же объекту настроек, исключена ситуация, что, например, одна функция пытается послать e-mail через sendmail, а другая через smtp. Можно, конечно, читать конфиги по требованию, но, имхо, это не всегда удобно/оправдано.
  • 0
    Добротный анализ, уважаю.
    Сравнил для себя с возможностями в Java: Статические классы не поддерживаются на уровне языка, они там тоже используются для тех же целей. Забавно, что через protected-конструктор можно обеспечить наследование в разных пакетах и для статических классов. А singleton-ы в Java удобно делать через enum.
  • –3
    Вот оно, большое программирование…
  • 0
    Кое-чего не хватает.
    Кстати, статический класс — это название специфично для .NET, а вообще этот паттерн называется Monostate.
    Так вот, если нужна какая-то инициализация перед использованием (например, открыть файл перед использованием лога), то у Singleton это легко решается — помещаем инициализацию в конструктор, он вызовется при первом обращении к instance. Если произойдет исключение — оно будет поймано и обработано нормальным образом. А вот у Monostate с этим хуже — конструктора у него нет, а исключение в конструкторе статического поля приведет к тому, что прога упадет и точная причина будет неизвестна.
    • +1
      … при этом у статического класса в .net конструктор вполне есть.
  • +4
    Есть такой тонкий момент, о котором почему-то не все знают.

    Статический класс не имеет к ООП вообще никакого отношения. Фактически, статический класс — это просто неймспейс с набором данных и функций. Естественно, так как это не класс, то никакого наследования, интерфейсов и прочих ООП-шных вещей, у него быть не может.

    В С++ с этим попроще, там допустимо как

    namespace SomeStaticData { int Data; int GetData() { return Data; }}

    так и

    class SomeStaticData {public: static int Data; static int GetData() {return Data; }}

    Конечно, в последнем случае допустима простейшая инкапсуляция, то есть сокрытие публичных полей, но это не делает неймспейс классом.
  • –1
    Могу еще одно преимущество синглтона привести, которое выгодно при использовании, например Qt или других языков/библиотек, позволяющих что-то типа сигналов/слотов. Можно иметь механизм глобального оповещения об изменениях (или других событиях) в синглтоне. Например, я использую синглтон для менеджера конфигурации. К нему обоащается окно Preferences, теневое обновление конфигурации с сервера, ну и конечно другие компоненты системы. Последние должны знать когда конфигурация изменилась, независимо от причины изменения. При использовании синглтона создаваемый интерфейс всегда подключается к синглтону и генерирует события уже внешним объектам. Как-то так.
    • –1
      В Qt QApplication фактически является синглтоном, что более, чем оправданно, сколько бы потоков не было в любом случае наличие нескольких QApplication'ов это логическая ошибка. Опять же к примеру хочется визуальные уведомления делать: вполне естественным сделать менеджер, который ими управляет синглтоном: ну не должно быть нескольких менеджеров.
  • +1
    Статический класс — всего лишь контейнер для отдельных подпрограмм, которые для красоты обозваны статическими методами и все тех же глобальных переменных, которые для красоты же обозваны статическими полями. Синглтон — тоже эквивалент глобальной переменной.
    На практике имеет смысл использовать статический класс как пространство имен для отдельных подпрограмм, а синглтон лучше вообще не использовать: класс, контролирующий количество своих экземпляров множит на ноль ортогональность, тестируемость и масштабируемость. Для контроля количества экземпляров гораздо лучше использовать фабрики (или другие порождающие паттерны).

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.