Menu
Coddy logo textTech

Паттерны неизменяемости

Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy — урок 34 из 70.

Неизменяемость доводит инкапсуляцию до логического предела — как только объект создан, он больше никогда не может быть изменен. Это устраняет целые категории багов, так как вам больше не нужно беспокоиться о том, что кто-то или что-то неожиданно изменит ваши данные.

Самый простой способ создать неизменяемый класс — использовать поля readonly с конструктором, который устанавливает все значения, и не предоставлять сеттеры:

public class Point
{
    public readonly int X;
    public readonly int Y;
    
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

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

public Point Move(int deltaX, int deltaY)
{
    return new Point(X + deltaX, Y + deltaY);
}

Исходная точка остается неизменной, в то время как вы получаете новую точку с обновленными значениями. Этот паттерн используется во всем .NET — строки, например, являются неизменяемыми. Когда вы вызываете ToUpper(), вы получаете новую строку, а не изменяете исходную.

Неизменяемые объекты потокобезопасны по своей природе, так как никакой код не может изменить их после создания. О них также легче рассуждать, поскольку вы знаете, что их состояние никогда не изменится неожиданно.

challenge icon

Задание

Легко

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

Вы организуете свой код в двух файлах:

  • Money.cs: Создайте неизменяемый класс Money в пространстве имен Finance. Ваши деньги должны иметь:
    • Два поля readonly: Amount (decimal) и Currency (string)
    • Конструктор, который устанавливает оба значения — после создания объект Money никогда не может измениться
    • Метод Add(decimal value), который возвращает новый объект Money с увеличенной суммой (та же валюта)
    • Метод Subtract(decimal value), который возвращает новый объект Money с уменьшенной суммой (та же валюта)
    • Метод Convert(string newCurrency, decimal rate), который возвращает новый объект Money с суммой, умноженной на курс (rate), и новой валютой
    • Метод GetDisplay(), который возвращает строку в формате "{Amount} {Currency}"
    Помните: ни один из этих методов не должен изменять исходный объект — все они возвращают совершенно новые экземпляры Money!
  • Program.cs: В основном файле продемонстрируйте неизменяемость в действии. Создайте начальный объект Money, затем выполните над ним операции, сохраняя ссылки как на оригинал, так и на результаты. Это докажет, что оригинал никогда не меняется даже после нескольких операций.

Вы получите четыре входных значения:

  • Начальная сумма (decimal)
  • Начальная валюта (string)
  • Сумма для добавления (decimal)
  • Курс конвертации в EUR (decimal)

Выведите результат в следующем формате:

Original: {GetDisplay()}
After add: {GetDisplay()}
Original unchanged: {GetDisplay()}
Converted: {GetDisplay()}

Например, если входные данные 100.00, USD, 50.00 и 0.85, вывод должен быть таким:

Original: 100.00 USD
After add: 150.00 USD
Original unchanged: 100.00 USD
Converted: 127.50 EUR

Обратите внимание, что исходный объект Money по-прежнему показывает 100.00 USD даже после вызова Add() — это и есть неизменяемость! Метод Add() создал совершенно новый объект с 150.00 USD, оставив оригинал нетронутым. Конвертация выполняется над результатом операции сложения.

Шпаргалка

Неизменяемость (immutability) означает, что после создания объекта его состояние не может быть изменено. Это исключает ошибки, связанные с неожиданной модификацией данных.

Создайте неизменяемый класс, используя поля readonly с конструктором и без сеттеров:

public class Point
{
    public readonly int X;
    public readonly int Y;
    
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Чтобы «изменить» неизменяемый объект, верните новый экземпляр вместо изменения существующего:

public Point Move(int deltaX, int deltaY)
{
    return new Point(X + deltaX, Y + deltaY);
}

Преимущества неизменяемых объектов:

  • Потокобезопасность по определению, так как никакой код не может изменить их после создания
  • О них легче рассуждать, так как их состояние никогда не меняется неожиданно
  • Широко используются в .NET (например, строки являются неизменяемыми)

Попробуйте сами

using System;
using Finance;

class Program
{
    public static void Main(string[] args)
    {
        // Чтение входных данных
        decimal initialAmount = Convert.ToDecimal(Console.ReadLine());
        string initialCurrency = Console.ReadLine();
        decimal amountToAdd = Convert.ToDecimal(Console.ReadLine());
        decimal conversionRate = Convert.ToDecimal(Console.ReadLine());

        // TODO: Создайте начальный объект Money с начальной суммой и валютой

        // TODO: Выведите исходные деньги, используя GetDisplay()

        // TODO: Вызовите Add() для оригинала и сохраните результат в новую переменную

        // TODO: Выведите результат после сложения, используя GetDisplay()

        // TODO: Снова выведите оригинал, чтобы доказать, что он не изменился

        // TODO: Конвертируйте полученный результат в EUR, используя курс конвертации

        // TODO: Выведите конвертированные деньги, используя GetDisplay()
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование