Допустим, мы решили выражать арифметические операции объектами.
public final class Sum implements IntBinaryOperator {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
}
class Sum : (Int, Int) -> Int {
override fun invoke(left: Int, right: Int): Int =
left + right
}
Пример использования:
new Sum().applyAsInt(42, 24);
Sum()(42, 24)
У этого класса есть важное свойство: все его экземпляры одинаковы. Превратим его в синглтон — пусть существует только один его экземпляр:
public final class Sum implements IntBinaryOperator {
private static final Sum INSTANCE = new Sum();
public static Sum getInstance() {
return INSTANCE;
}
private Sum() {
// приватный конструктор
// запрещает создание извне
}
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
}
object Sum : (Int, Int) -> Int {
override fun invoke(left: Int, right: Int): Int =
left + right
}
Пример использования:
Sum.getInstance().applyAsInt(42, 24);
Sum(42, 24)
Такая маленькая хитрость идёт на пользу производительности, но есть несколько способов всё испортить.
Антипаттерн: синглтон с зависимостями
Напишем то же самое для Byte, опираясь на существующую реализацию Sum для int.
public final class ByteSum implements BinaryOperator<Byte> {
private static final ByteSum INSTANCE = new ByteSum();
public static ByteSum getInstance() {
return INSTANCE;
}
private ByteSum() {}
@Override
public Byte apply(Byte left, Byte right) {
return (byte) Sum.getInstance().applyAsInt(left, right);
}
}
object ByteSum : (Byte, Byte) -> Byte {
override fun invoke(left: Byte, right: Byte): Byte =
Sum(left.toInt(), right.toInt()).toByte()
}
Здесь у ByteSum есть зависимость — Sum. Таким образом, для другой операции (например, Diff), пусть и реализующей тот же интерфейс, придётся писать другую обёртку (ByteDiff).
По-хорошему, класс не должен знать о своих зависимостях, они должны передаваться через конструктор. Исправляем:
public final class IntOperatorByteAdapter implements BinaryOperator<Byte> {
private final IntBinaryOperator operator;
public IntOperatorByteAdapter(IntBinaryOperator operator) {
this.operator = operator;
}
@Override
public Byte apply(Byte left, Byte right) {
return (byte) operator.applyAsInt(left, right);
}
}
class IntOperatorByteAdapter(
private val intOperator: (Int, Int) -> Int
) : (Byte, Byte) -> Byte {
override fun invoke(left: Byte, right: Byte): Byte =
intOperator(left.toInt(), right.toInt()).toByte()
}
Теперь класс не разрешает свои зависимости самостоятельно, они передаются в конструктор. Соответственно, вместо ByteSum, ByteDiff, ByteMultiplication, ByteDivision будет IntOperatorByteAdapter.
Антипаттерн: синглтон с состоянием
Допустим, нужно считать количество активных соединений:
public final class Application {
private static final Application INSTANCE = new Application();
public static Application getInstance() {
return INSTANCE;
}
private Application() {
}
private int connections = 0;
public void connected() {
connections++;
}
public void disconnected() {
connections--;
}
}
object Application {
private var connections = 0
fun connected() {
connections++
}
fun disconnected() {
connections--
}
}
Соответственно, внутри проекта встречаются такие строки:
Application.getInstance().connected();
try {
...
} finally {
Application.getInstance().disconnected();
}
Application.connected()
try {
...
} finally {
Application.disconnected()
}
Проблемы:
- Переиспользуемость кода понижается: компоненты, которые используют Application, изменяют глобальное состояние, как бы мы их ни использовали.
- Синглтон невозможно подменить для тестирования. Тесты начинают зависеть от глобального состояния.
- Любой код может использовать синглтон, поэтому сложнее контролировать, из каких потоков он используется и не изменяют ли его из разных потоков одновременно.
Если превратить этот синглтон в нормальный класс, получим что-то такое:
public final class ConnectionCount {
private int connections = 0;
public void connected() {
connections++;
}
public void disconnected() {
connections--;
}
}
class ConnectionCount {
private var connections = 0
fun connected() {
connections++
}
fun disconnected() {
connections--
}
}
Далее создадим экземпляр и передадим его тем классам, которым он необходим:
ConnectionCount cc = new ConnectionCount();
new SomeComponent(
someDependency,
new Something(),
cc,
...
);
val cc = ConnectionCount()
SomeComponent(
someDependency,
Something(),
cc,
...
)
Рассмотрим, применимы ли проблемы синглтона к этому решению.
- Глобального состояния больше нет. Разным компонентам при необходимости можно передать разные экземпляры ConnectionCount.
- При тестировании можно создать локальный экземпляр, доступный только одному тесту.
- При создании объектов передаём ConnectionCount явно, что позволяет проще определить, используется ли он из разных потоков, и внести соответствующие поправки.
Антипаттерн: ленивая инициализация синглтона
Сама по себе ленивая инициализация, т. е. инициализация при первом обращении — хороший шаблон. Рассмотрим его в контексте синглтонов.
public final class Sloth {
private static Sloth instance;
private Sloth() {}
public static Sloth getInstance() {
if (instance == null) {
instance = new Sloth();
}
return instance;
}
}
class Sloth private constructor() {
// ...
private companion object {
val instance: Sloth
get() {
if (_instance == null) {
_instance = Sloth()
}
return _instance!!
}
private var _instance: Sloth? = null
}
}
Проблема первая: этот код не потокобезопасен. Если из разных потоков запросить instance одновременно, можно увидеть разные значения. Исправляем:
public synchronized Sloth getInstance() {
@Synchronized
fun getInstance(): Sloth {
Теперь появляется другая проблема: блокировка захватывается при каждом вызове getInstance, даже если instance давно инициализирован. Применим double-checking, чтобы захватывать блокировку только при инициализации.
public static Sloth getInstance() {
if (instance == null) {
synchronized (Sloth.class) {
if (instance == null) {
Sloth sloth = new Sloth();
instance = sloth;
return sloth;
}
}
}
return instance;
}
val instance: Sloth
get() {
if (_instance == null) {
synchronized(this) {
if (_instance == null) {
val sloth = Sloth()
_instance = sloth
return sloth
}
}
}
return _instance!!
}
Теперь у нас есть уродливый, зато безопасный (если честно, не совсем) код.
Классы в JVM (и в Android) загружаются лениво, т. е. по первому требованию. При загрузке класса виртуальная машина самостоятельно удерживает блокировку, поэтому даже в условии гонки, когда несколько потоков требуют загрузки одного класса, инициализатор класса выполнится ровно один раз. Это значит, что «ручная» ленивая инициализация синглтона бесполезна.
Правило
У синглтона нет ни зависимостей, ни состояния, в противном случае он не должен быть синглтоном. Ленивую и безопасную инициализацию обеспечивает загрузчик классов.