Предупреждений против использования синглетонов множество. Шаблон проектирования ухудшает тестируемость программного обеспечения. Методология поощряет включение классов, которые могут измениться, что делает их изменение чрезвычайно рискованным. Тем не менее, синглтон по-прежнему широко используется. Этот мотив будет по-прежнему широко использоваться разработчиками. Будут использоваться синглтоны, реальность, которая требует совета, а не предостережения. Программисты могут использовать синглтон менее вредным образом, если инженеры воспользуются рекомендациями, приведенными ниже.

Не помещайте сложную логику в конструктор

Конструктор синглтона должен содержать простую логику. Невозможно избежать ассемблера архетипа. Его инструкции должны быть выполнены, прежде чем разработчик сможет использовать экземпляр своего класса. Если конструктор типа дает сбой, то программист не может использовать какие-либо данные или методы этого объекта. В конкретном случае конструктор является точкой отказа. Это место увеличивается по мере увеличения сложности логики ассемблера. Сложные инструкции в конструкторе делают его особенно опасным местом. Эта опасность особенно актуальна для синглетонов, которые делают один вызов своего ассемблера в программе. Любая другая ссылка на этот класс потенциально приводит к ошибке или к нескольким ошибкам, если конструктор терпит неудачу. Сложный ассемблер с большей вероятностью потерпит неудачу, что дает достаточные основания для того, чтобы синглтоны имели простую логику сборки.

internal sealed class ComplexConstructor
{
    private static ComplexConstructor _instance = null;
    private Dictionary<string, string> _data;
 
    //Puts file reading in the constructor
    private ComplexConstructor() {
        initialize();
    }
 
    public static ComplexConstructor Instance {
        get {
            if (_instance == null) {
                _instance = new ComplexConstructor();
            }
            return _instance;
        }
    }
 
    public string getData(string key)
    {
        return _data[key];
    }
 
    private void initialize()
    {
        _data = readDataFromFile(ConfigurationManager.AppSettings["Data.File.Path"]);
    }
 
    private Dictionary<string, string> readDataFromFile(string path)
    {
        Dictionary<string, string> data = new Dictionary<string, string>();
 
        using (StreamReader reader = getFileStream(path))
        {
            readDataIntoTable(reader, data);
        }
 
        return data;
    }
 
    private static StreamReader getFileStream(string path)
    {
        return new StreamReader(path);
    }
 
    private void readDataIntoTable(StreamReader reader, Dictionary<string, string> table)
    {
        //process file
    }
}
internal sealed class SimpliefiedConstructor
{
    private static SimpliefiedConstructor _instance = null;
    private Dictionary<string, string> _data;
 
    private SimpliefiedConstructor()
    {
        _data = null;
    }
 
    //Moves file reading from the constructor to the getter
    public static SimpliefiedConstructor Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new SimpliefiedConstructor();
                _instance.initialize();
            }
            return _instance;
        }
    }
 
    public string getData(string key)
    {
        return _data[key];
    }
 
    private void initialize()
    {
        _data = readDataFromFile(ConfigurationManager.AppSettings["Data.File.Path"]);
    }
 
    private Dictionary<string, string> readDataFromFile(string path)
    {
        Dictionary<string, string> data = new Dictionary<string, string>();
 
        using (StreamReader reader = getFileStream(path))
        {
            readDataIntoTable(reader, data);
        }
 
        return data;
    }
 
    private static StreamReader getFileStream(string path)
    {
        return new StreamReader(path);
    }
 
    private void readDataIntoTable(StreamReader reader, Dictionary<string, string> table)
    {
        //process the file
    }
}

Не помещайте сложную логику в экземпляр Getter

Инструкции в геттере синглтона должны быть несложными. Функция извлечения должна быть вызвана, прежде чем разработчик сможет использовать возможности класса. Если геттер этого шаблона дает сбой, то функциональность синглтона никогда не вызывается. Возможности класса заблокированы из-за того, что они используются или проверяются его неисправным методом извлечения. Для этой агрегации функций и/или данных геттер со сложной логикой является серьезным препятствием на пути к стабильности и тестируемости. Любое изменение этих инструкций может затруднить тестирование метода или его использование, что сделает класс чрезвычайно уязвимым. Выносливые, надежные одноэлементные типы избегают сложных рассуждений в своих геттерах.

internal sealed class ComplexGetter
{
    private static ComplexGetter _instance = null;
    private Dictionary<string, string> _data;
 
    private ComplexGetter()
    {
        _data = null;
    }
 
    //Contains file reading logic in its getter
    public static ComplexGetter Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new ComplexGetter();
                _instance.initialize();
            }
            return _instance;
        }
    }
 
    public string getData(string key)
    {
        return _data[key];
    }
 
    private void initialize()
    {
        _data = readDataFromFile(ConfigurationManager.AppSettings["Data.File.Path"]);
    }
 
    private Dictionary<string, string> readDataFromFile(string path)
    {
        Dictionary<string, string> data = new Dictionary<string, string>();
 
        using (StreamReader reader = getFileStream(path))
        {
            readDataIntoTable(reader, data);
        }
 
        return data;
    }
 
    private static StreamReader getFileStream(string path)
    {
        return new StreamReader(path);
    }
 
    private void readDataIntoTable(StreamReader reader, Dictionary<string, string> table)
    {
        //process file
    }
}
internal sealed class SimplifiedGetter
{
    private readonly static SimplifiedGetter _instance =  new SimplifiedGetter();
    private Dictionary<string, string> _data;
    private bool _isInitialized;
 
    private SimplifiedGetter()
    {
        _data = null;
        _isInitialized = false;
    }
 
    //getter is simplified to return data and do nothing else
    public static SimplifiedGetter Instance
    {
        get
        {
            return _instance;
        }
    }
 
    //This method can always be made to throw an exception, if initialize has not been called
    public string getData(string key)
    {
        return _data[key];
    }
 
    //file read logic is moved to an optional method
    public void initialize()
    {
        if (_isInitialized)
        {
            return;
        }
 
        _data = readDataFromFile(ConfigurationManager.AppSettings["Data.File.Path"]);
 
        _isInitialized = true;
    }
 
    private Dictionary<string, string> readDataFromFile(string path)
    {
        Dictionary<string, string> data = new Dictionary<string, string>();
 
        using (StreamReader reader = getFileStream(path))
        {
            readDataIntoTable(reader, data);
        }
 
        return data;
    }
 
    private static StreamReader getFileStream(string path)
    {
        return new StreamReader(path);
    }
 
    private void readDataIntoTable(StreamReader reader, Dictionary<string, string> table)
    {
        //process file
    }
}

Используйте объектно-ориентированные принципы, такие как наследование

Надежные синглтоны используют такие объектно-ориентированные (ОО) концепции, как наследование. Шаблон может использовать наследование для расширения уже существующей функциональности. Эту возможность можно протестировать отдельно от архетипа, что делает проверку модульной. Раздельная проверка имеет тенденцию находить больше ошибок. Когда обнаруживается больше проблем, программист устраняет больше проблем. С меньшими сложностями инженер производит более прочные синглтоны. Классы Hardy, как правило, пользуются такими объектно-ориентированными методами, как наследование.

internal abstract class DataHolder
{
    private Dictionary<string, string> _data;
    private bool _isInitialized;
 
    protected DataHolder()
    {
        _data = null;
        _isInitialized = false;
    }
 
    //common iniitalization structure
    public void initialize()
    {
        if (_isInitialized)
        {
            return;
        }
 
        initializeData();
 
        _isInitialized = true;
    }
 
    //gettting the data is commmon
    public string getData(string key)
    {
        return _data[key];
    }
 
    private void initializeData()
    {
        createDataTable();
        loadDataIntoTable();
    }
 
    private void createDataTable()
    {
        _data = new Dictionary<string, string>();
    }
 
    private void loadDataIntoTable()
    {
        IEnumerable<string[]> dataItems = getDataItems();
        bindDataItems(dataItems);
    }
 
    //specialized based on implementation
    protected abstract IEnumerable<string[]> getDataItems();
 
    //loading into dictionary is common
    private void bindDataItems(IEnumerable<string[]> dataItems)
    {
        foreach(string[] dataItem in dataItems)
        {
            _data.Add(dataItem[0], dataItem[1]);
        }
    }
}
internal sealed class HardCodedDataHolder : DataHolder
    {
        private static readonly HardCodedDataHolder _instance = new HardCodedDataHolder();
 
        private HardCodedDataHolder() { }
 
        public static HardCodedDataHolder Instance
        {
            get
            {
                return _instance;
            }
        }
 
        //produces a hardcoded list
        protected override IEnumerable<string[]> getDataItems()
        {
            List<string[]> items = new List<string[]>();
 
            //add hardcoded items
 
            return items;
        }
    }
internal sealed class FileBasedDataHolder : DataHolder
    {
        private static readonly FileBasedDataHolder _instance = new FileBasedDataHolder();
        private FileBasedDataHolder() { }
 
        public static FileBasedDataHolder Instance { get { return _instance; } }
 
        //produces a list based on data from a file
        protected override IEnumerable<string[]> getDataItems()
        {
            List<string[]> items = null;
            using (StreamReader reader = getFileStream())
            {
                items = readData(reader);
            }
            return items;
        }
 
        private StreamReader getFileStream()
        {
            return new StreamReader(ConfigurationManager.AppSettings["Data.File.Path"]);
        }
 
        private List<string[]> readData(StreamReader reader)
        {
            List<string[]> items = new List<string[]>();
            //process file
            return items;
        }
    }

Пересмотрите любой синглтон, который является приглашением или становится приглашением стать классом бога.

Хорошо спроектированный синглтон не должен быть божественным классом. Тип, который делает все или может быть разумно расширен, чтобы делать что угодно, является приглашением к интенсивному расширению и постоянной модификации, что делает кодовую базу этого класса хрупкой. Любая программа с одним или несколькими божественными классами легко ломается. Эти типы поощряют модификацию. Когда происходит редактирование, приложение рискует вызвать каскадные проблемы. Эти проблемы могут быть нетривиальными для исправления. Препятствий, связанных с божественными классами, можно избежать, если вообще не создавать такой тип. Если классы не должны делать все, то и синглтоны тоже не должны.

//this class is a magnet for fan-in and for adding methods or modifying functionality
    internal sealed class GeneralDataAccessHandler
    {
        private static readonly GeneralDataAccessHandler _instance = new GeneralDataAccessHandler();
        private GeneralDataAccessHandler() { }
 
        public static GeneralDataAccessHandler Instance { get { return _instance; } }
 
        public DataTable getUsers(string company)
        {
            //get users for a company
            return null;
        }
 
        public DataTable getCompanies()
        {
            //get a list of companies
            return null;
        }
 
        public DataTable getRecords(string user)
        {
            //get records for users
            return null;
        }
    }

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

Разработчик должен разбивать синглтоны на удобоваримые части и размещать их в слоях, которые используют преимущества классов более низких уровней. Типы должны быть сфокусированы, и они должны использовать другие типы для функциональности, выходящей за рамки их конкретной разновидности. Небольшие классы легче тестировать. Концентрированные типы имеют тенденцию не сильно расти. Многоуровневые наборы данных и методов обеспечивают стабильность с течением времени. Если разнообразие зависит от стабильного класса, то этому виду нужно сосредоточиться только на тестировании самого себя. Проверка этого типа предполагает, что используемые им категории уже были проверены, поэтому ему нужно сосредоточиться только на собственной оценке. Подразделенные и многоуровневые синглтоны облегчают работу программиста.

//handles sql at a low level
    internal sealed class SqlHandler
    {
        private static readonly SqlHandler _instance = new SqlHandler();
        private SqlHandler() { }
 
        public static SqlHandler Instance { get { return _instance; } }
 
        public DataTable getData(string sql)
        {
            return null; //executes the SQL and returns a data table
        }
    }
internal interface IDatabaseObject
    {
        //fill in with API for putting a database row into
        // an object and producting SQL queries from an object
    }
//handles data access at an object level
    internal sealed class DataAccessHandler
    {
        private static readonly DataAccessHandler _instance = new DataAccessHandler();
        private DataAccessHandler() { }
 
        public static DataAccessHandler Instance { get { return _instance; } }
 
        public List<DataObjectType> getData<DataObjectType>(string sql)
            where DataObjectType : IDatabaseObject
        {
            DataTable rawData = getRawData(sql);
            return getData<DataObjectType>(rawData);
        }
 
        private static DataTable getRawData(string sql)
        {
            return SqlHandler.Instance.getData(sql);
        }
 
        private List<DataObjectType> getData<DataObjectType>(DataTable rawData)
            where DataObjectType : IDatabaseObject
        {
            return null; //builds out the data objects and binds the rows to them
        }
    }
internal interface ISytemUser : IDatabaseObject
    {
        //fill in details
    }
internal interface IUserRecord : IDatabaseObject
    {
        //fill in details
    }
//focuses only on functionalityu related to user data
    internal sealed class UserDataHandler
    {
        private static readonly UserDataHandler _instance = new UserDataHandler();
        private UserDataHandler() { }
 
        public static UserDataHandler Instance { get { return _instance; } }
 
        public List<ISytemUser> getUsers(string company)
        {
            string sql = getUserSql(company);
            return getData<ISytemUser>(sql);
        }
 
        private string getUserSql(string company)
        {
            //generate the SQL statement that gets the users for a company
            return null;
        }
 
        public List<IUserRecord> getRecords(string user)
        {
            string sql = getRecordsSql(user);
            return getData<IUserRecord>(sql);
        }
 
        private string getRecordsSql(string user)
        {
            //generate the SQL that gets the records for a user
            return null;
        }
 
        private List<DataObjectType> getData<DataObjectType>(string sql)
            where DataObjectType : IDatabaseObject
        {
            return DataAccessHandler.Instance.getData<DataObjectType>(sql);
        }
    }
//focuses only on company data
    internal sealed class CompanyDataHandler
    {
        private static readonly CompanyDataHandler _instance = new CompanyDataHandler();
        private CompanyDataHandler() { }
 
        public static CompanyDataHandler Instance { get { return _instance; } }
 
        public List<ICompany> getCompanies()
        {
            string sql = getCompanySql();
            return getData<ICompany>(sql);
        }
 
        private string getCompanySql()
        {
            //generate the SQL statement that gets a list of companies
            return null;
        }
 
        private List<DataObjectType> getData<DataObjectType>(string sql)
            where DataObjectType : IDatabaseObject
        {
            return DataAccessHandler.Instance.getData<DataObjectType>(sql);
        }
    }

Резюме

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

Монолит от tangi bertin под лицензией CC BY 2.0