Тут важливо розрізняти окремі екземпляри та шаблон дизайну Singleton .
Поодинокі випадки - це просто реальність. Більшість додатків розроблені лише для роботи з однією конфігурацією, одним інтерфейсом користувача одночасно, однією файловою системою тощо. Якщо потрібно підтримувати багато стану або даних, то, звичайно, ви хочете мати лише один екземпляр і тримати його в живих якомога довше.
Шаблон дизайну Singleton - це дуже специфічний тип одиничного екземпляра, зокрема такий:
- Доступний через глобальне статичне поле екземпляра;
- Створено або при ініціалізації програми, або при першому доступі;
- Жоден публічний конструктор (не може створювати інстанцію безпосередньо);
- Ніколи не явно звільнений (неявно звільнений після закінчення програми).
Саме завдяки цьому вибору конкретного дизайну цей шаблон представляє кілька потенційних довгострокових проблем:
- Неможливість використання абстрактних або інтерфейсних класів;
- Нездатність підкласу;
- Висока зв'язок у додатку (важко модифікувати);
- Важко перевірити (не можна підробити / знущатися в одиничних тестах);
- Важко паралелізувати у випадку стану, що змінюється (потребує великого блокування);
- і так далі.
Жоден із цих симптомів насправді не є ендемічним для одиничних випадків, лише закономірність Сінглтона.
Що ти можеш зробити замість цього? Просто не використовуйте шаблон Singleton.
Цитуючи з питання:
Ідея полягала в тому, щоб це було одне місце в додатку, який зберігає дані, що зберігаються та синхронізуються, і тоді будь-які нові екрани, які відкриваються, можуть просто запитувати більшу частину того, що їм потрібно там, не роблячи повторних запитів на різні підтримуючі дані від сервера. Постійне звернення до сервера вимагало б занадто великої пропускної спроможності - і я кажу за тисячі доларів додаткових рахунків за Інтернет в тиждень, так що це було неприпустимо.
Ця концепція має назву, як ви натякаєте, але звучить невизначено. Це називається кеш . Якщо ви хочете пофантазувати, ви можете назвати це "кеш офлайн" або просто офлайнова копія віддалених даних.
Кеш не повинен бути одиночним. Це може повинен бути один екземпляр , якщо ви хочете , щоб уникнути вибірки одні і ті ж дані для декількох екземплярів кешу; але це не означає, що ви насправді маєте викривати все перед усім .
Перше, що я зробив би - це розділити різні функціональні області кешу на окремі інтерфейси. Наприклад, скажімо, ви робили найгірший у світі клон YouTube на основі Microsoft Access:
MSAccessCache
▲
|
+ ----------------- + ----------------- +
| | |
IMediaCache IProfileCache IPageCache
| | |
| | |
VideoPage MyAccountPage MostPopularPage
Тут у вас є декілька інтерфейсів, що описують конкретні типи даних, до яких певному класу може знадобитися доступ - медіа, профілі користувачів та статичні сторінки (як-от лицьова сторінка). Все це реалізовано одним мега-кешем, але ви розробляєте свої індивідуальні класи, щоб замість цього приймати інтерфейси, тому їм не байдуже, який екземпляр у них є. Ви ініціалізуєте фізичний екземпляр один раз, коли ваша програма запускається, а потім просто починаєте передавати екземпляри (передані певному типу інтерфейсу) через конструктори та загальнодоступні властивості.
Це , до речі, називається вприскуванням залежності ; вам не потрібно використовувати Spring або будь-який спеціальний контейнер IoC, до тих пір, поки ваш загальний дизайн класу приймає його залежність від абонента, а не інстанціювати їх самостійно або посилатися на глобальний стан .
Навіщо використовувати інтерфейс на основі інтерфейсу? Три причини:
Це полегшує читання коду; ви можете чітко зрозуміти з інтерфейсів, від яких даних залежать залежні класи.
Якщо і коли ви зрозумієте, що Microsoft Access не був найкращим вибором для резервного копіювання даних, можете замінити його чимось кращим - скажімо, SQL Server.
Якщо і коли ви зрозумієте, що SQL Server не є найкращим вибором для медіа конкретно , ви можете зламати свою реалізацію, не впливаючи на будь-яку іншу частину системи . Саме тут надходить реальна сила абстракції.
Якщо ви хочете зробити це на крок далі, ви можете використовувати контейнер IoC (рамки DI), наприклад Spring (Java) або Unity (.NET). Практично кожен фреймворк DI буде робити своє власне управління життям і конкретно дозволяє вам визначити певну послугу як єдиний екземпляр (часто називаючи це "синглтон", але це лише для ознайомлення). В основному ці рамки економлять більшу частину роботи мавпи вручну проходження екземплярів, але вони не є суворо необхідними. Для втілення цієї конструкції вам не потрібні спеціальні інструменти.
Для повноти слід зазначити, що вищезазначений дизайн насправді теж не ідеальний. Коли ви маєте справу з кешем (як і ви), ви насправді повинні мати зовсім окремий шар . Іншими словами, такий дизайн:
+ - IMediaRepository
|
Кеш (загальний) --------------- + - IProfileRepository
▲ |
| + - IPageRepository
+ ----------------- + ----------------- +
| | |
IMediaCache IProfileCache IPageCache
| | |
| | |
VideoPage MyAccountPage MostPopularPage
Перевага цього полягає в тому, що вам навіть не потрібно розбивати свій Cache
примірник, якщо ви вирішили переробити його; ви можете змінити спосіб зберігання медіа просто, подаючи його альтернативну реалізацію IMediaRepository
. Якщо ви подумаєте, як це вписується разом, ви побачите, що він все ще коли-небудь створює один фізичний примірник кешу, тому вам ніколи не потрібно буде отримувати одні й ті самі дані двічі.
Нічого з цього не означає, що кожен програмний продукт у світі повинен бути зосереджений на цих вимогливих стандартах високої згуртованості та слабкої зв'язку; це залежить від розміру та обсягу проекту, вашої команди, бюджету, термінів і т. д. Але якщо ви запитуєте, який найкращий дизайн (використовувати замість одинарного), то це все.
PS Як заявили інші, мабуть, не найкраща ідея для залежних класів усвідомлювати, що вони використовують кеш , - це деталь реалізації, про яку вони ніколи не повинні піклуватися. Якщо говорити, загальна архітектура все одно буде виглядати дуже схоже на те, що зображено вище, ви просто не посилатиметесь на окремі інтерфейси як кеші . Замість цього ви б назвали їх Послуги або щось подібне.