Javanese Online

Singleton (Одиночка)

Язык примеров

Допустим, мы решили выражать арифметические операции объектами.

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()
}

Проблемы:

Если превратить этот синглтон в нормальный класс, получим что-то такое:

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,
    ...
)

Рассмотрим, применимы ли проблемы синглтона к этому решению.

Антипаттерн: ленивая инициализация синглтона

Сама по себе ленивая инициализация, т. е. инициализация при первом обращении — хороший шаблон. Рассмотрим его в контексте синглтонов.

public final class Sloth {

    private static Sloth instance;

    private Sloth() {}

    public 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 Sloth getInstance() {
    if (instance == null) {
        synchronized (this) {
            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) загружаются лениво, т. е. по первому требованию. При загрузке класса виртуальная машина самостоятельно удерживает блокировку, поэтому даже в условии гонки, когда несколько потоков требуют загрузки одного класса, инициализатор класса выполнится ровно один раз. Это значит, что «ручная» ленивая инициализация синглтона бесполезна.

Правило

У синглтона нет ни зависимостей, ни состояния, в противном случае он не должен быть синглтоном. Ленивую и безопасную инициализацию обеспечивает загрузчик классов.

Комментарии к уроку

Сообщить об ошибке

Javanese.Online в GitHub

Чаты и каналы в Telegram

RSS-лента