Перейти к содержимому

Практика 1. Классы, объекты и инкапсуляция в Swift

Цель

Закрепить основные понятия первой лекции: проектирование классов, создание экземпляров, хранимые и вычисляемые свойства, методы экземпляра и типа, инициализаторы, инкапсуляцию и ссылочную семантику.

По итогам работы вы должны уметь описать предметную сущность классом, определить её корректное состояние и предоставить безопасный интерфейс для изменения этого состояния.


Среда и запуск

Для работы нужен Swift 6 или новее.

Проверка версии:

Окно терминала
swift --version

Каждое задание можно выполнять в отдельном файле task_N.swift и запускать как скрипт:

Окно терминала
swift task_1.swift

Либо скомпилировать программу:

Окно терминала
swiftc task_1.swift -o task_1
./task_1

На Windows имя исполняемого файла будет task_1.exe.


Краткая теория

  • Класс описывает состояние и поведение объектов. Конкретный объект является экземпляром класса.
  • Хранимое свойство содержит значение. Вычисляемое свойство рассчитывает его с помощью get и при необходимости изменяет связанное состояние через set.
  • Инициализатор init обязан присвоить значения всем хранимым свойствам. Проваливающийся init? возвращает nil, если корректный объект создать нельзя.
  • private скрывает деталь реализации, а private(set) разрешает внешнему коду читать свойство, но запрещает менять его напрямую.
  • Методы экземпляра работают с конкретным объектом. Члены с static принадлежат самому типу.
  • Классы имеют ссылочную семантику: после присваивания две переменные могут ссылаться на один объект.
  • === проверяет тождественность объектов. == проверяет логическое равенство, если тип реализует Equatable.

Общие требования

  1. Все классы, от которых не требуется наследование, объявляйте как final.
  2. Не открывайте изменение состояния без необходимости. Используйте let, private и private(set).
  3. Проверяйте инварианты в инициализаторах и методах.
  4. Не используйте принудительное извлечение optional через ! в основной логике программы.
  5. После каждого задания добавьте проверки с помощью assert и несколько понятных вызовов print.
  6. Названия типов пишите в UpperCamelCase, свойств и методов — в lowerCamelCase.

Задание 1. Класс Rectangle — свойства и инициализация

Создайте класс прямоугольника.

Требования

  • width и height хранят стороны прямоугольника;
  • стороны должны быть больше нуля;
  • если переданы некорректные стороны, init? возвращает nil;
  • area и perimeter реализованы как вычисляемые свойства только для чтения;
  • isSquare показывает, является ли прямоугольник квадратом;
  • метод scale(by:) увеличивает или уменьшает обе стороны; коэффициент должен быть больше нуля;
  • внешний код может читать стороны, но не менять их напрямую.
final class Rectangle {
private(set) var width: Double
private(set) var height: Double
var area: Double {
fatalError("TODO")
}
var perimeter: Double {
fatalError("TODO")
}
var isSquare: Bool {
fatalError("TODO")
}
init?(width: Double, height: Double) {
fatalError("TODO")
}
func scale(by factor: Double) -> Bool {
fatalError("TODO")
}
}

Минимальные проверки:

let rectangle = Rectangle(width: 3, height: 4)
assert(rectangle?.area == 12)
assert(rectangle?.perimeter == 14)
assert(rectangle?.isSquare == false)
assert(Rectangle(width: 0, height: 4) == nil)
assert(rectangle?.scale(by: 2) == true)
assert(rectangle?.area == 48)
assert(rectangle?.scale(by: -1) == false)

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


Задание 2. Класс Counter — состояние экземпляра и типа

Покажите разницу между состоянием отдельного объекта и состоянием всего типа.

Требования

  • value — текущее значение конкретного счётчика;
  • менять value напрямую снаружи нельзя;
  • increment() увеличивает значение на один;
  • increment(by:) увеличивает значение на положительное число и возвращает false, если аргумент некорректен;
  • reset() возвращает счётчик к нулю;
  • createdCount хранит число успешно созданных экземпляров;
  • howManyCreated() возвращает значение createdCount.
final class Counter {
private(set) static var createdCount = 0
private(set) var value = 0
init() {
fatalError("TODO")
}
func increment() {
fatalError("TODO")
}
func increment(by amount: Int) -> Bool {
fatalError("TODO")
}
func reset() {
fatalError("TODO")
}
static func howManyCreated() -> Int {
fatalError("TODO")
}
}

Проверки должны показать, что значения двух счётчиков независимы, а createdCount является общим:

let firstCounter = Counter()
let secondCounter = Counter()
firstCounter.increment()
assert(firstCounter.value == 1)
assert(secondCounter.value == 0)
assert(Counter.howManyCreated() == 2)

Задание 3. Класс Temperature — вычисляемое свойство с get и set

Реализуйте температуру, которая хранится в градусах Цельсия, но может читаться и изменяться в градусах Фаренгейта.

Требования

  • celsius нельзя установить ниже абсолютного нуля -273.15;
  • некорректное начальное значение приводит к nil из init?;
  • изменять celsius можно только методом setCelsius(_:);
  • метод возвращает false и сохраняет прежнее состояние при некорректном значении;
  • fahrenheit — вычисляемое свойство с get и set;
  • сеттер fahrenheit переводит значение по формуле (f - 32) * 5 / 9 и также не нарушает абсолютный ноль.
final class Temperature {
private(set) var celsius: Double
var fahrenheit: Double {
get {
fatalError("TODO")
}
set {
// Переведите newValue в градусы Цельсия и проверьте результат.
}
}
init?(celsius: Double) {
fatalError("TODO")
}
@discardableResult
func setCelsius(_ value: Double) -> Bool {
fatalError("TODO")
}
}

Минимальные проверки:

let temperature = Temperature(celsius: 25)
assert(temperature?.fahrenheit == 77)
temperature?.fahrenheit = 32
assert(temperature?.celsius == 0)
let oldValue = temperature?.celsius
temperature?.fahrenheit = -500
assert(temperature?.celsius == oldValue)
assert(Temperature(celsius: -300) == nil)

Задание 4. Класс BankAccount — защищённые инварианты

Реализуйте банковский счёт, состояние которого нельзя сделать некорректным через публичный интерфейс.

Требования

  • number задаётся один раз и не может быть пустым;
  • balance доступен для чтения, но изменяется только методами класса;
  • начальный баланс не может быть отрицательным;
  • deposit(_:) принимает только положительную сумму;
  • withdraw(_:) принимает положительную сумму, не превышающую баланс;
  • оба метода возвращают Bool, показывающий успешность операции;
  • maskedNumber возвращает строку с последними четырьмя символами номера, например ****1234;
  • description() возвращает строку вида Счёт ****1234: 150.0.
final class BankAccount {
let number: String
private(set) var balance: Double
var maskedNumber: String {
fatalError("TODO")
}
init?(number: String, initialBalance: Double = 0) {
fatalError("TODO")
}
@discardableResult
func deposit(_ amount: Double) -> Bool {
fatalError("TODO")
}
@discardableResult
func withdraw(_ amount: Double) -> Bool {
fatalError("TODO")
}
func description() -> String {
fatalError("TODO")
}
}

Минимальные проверки:

let account = BankAccount(number: "408178101234", initialBalance: 100)
assert(account?.deposit(50) == true)
assert(account?.withdraw(30) == true)
assert(account?.balance == 120)
assert(account?.withdraw(1_000) == false)
assert(account?.balance == 120)
assert(account?.maskedNumber == "****1234")
assert(BankAccount(number: "", initialBalance: 0) == nil)
assert(BankAccount(number: "1234", initialBalance: -1) == nil)

Дополнительное условие: убедитесь, что строка из пробелов также не считается корректным номером счёта.


Задание 5. Класс LibraryMember — комплексное задание

Спроектируйте систему выдачи книг читателю. Это задание развивает мини-практику из первой лекции.

Начальная модель книги:

import Foundation
struct LibraryBook: Equatable {
let id: UUID
let title: String
}

Требования к LibraryMember

  • неизменяемые id и name;
  • borrowedBooks можно читать, но нельзя изменять напрямую;
  • один читатель может одновременно держать не более пяти книг;
  • borrow(_:) не добавляет одну и ту же книгу повторно;
  • borrow(_:) возвращает true при успешной выдаче;
  • returnBook(id:) удаляет книгу по идентификатору и сообщает результат;
  • borrowedCount — вычисляемое число взятых книг;
  • hasBorrowed(bookID:) отвечает, есть ли книга у читателя;
  • summary() возвращает имя читателя и названия всех взятых книг.
final class LibraryMember {
let id: UUID
let name: String
private(set) var borrowedBooks: [LibraryBook] = []
var borrowedCount: Int {
fatalError("TODO")
}
init(id: UUID = UUID(), name: String) {
self.id = id
self.name = name
}
@discardableResult
func borrow(_ book: LibraryBook) -> Bool {
fatalError("TODO")
}
@discardableResult
func returnBook(id: UUID) -> Bool {
fatalError("TODO")
}
func hasBorrowed(bookID: UUID) -> Bool {
fatalError("TODO")
}
func summary() -> String {
fatalError("TODO")
}
}

Проверка ссылочной семантики

После реализации обязательно выполните следующий эксперимент:

let member = LibraryMember(name: "Амина")
let sameMember = member
let book = LibraryBook(id: UUID(), title: "Swift Programming")
assert(member === sameMember)
assert(member.borrow(book) == true)
assert(sameMember.borrowedCount == 1)

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

Дополнительная часть

Создайте другого читателя с тем же именем и сравните ссылки через ===:

let anotherMember = LibraryMember(name: "Амина")
assert(member !== anotherMember)

Почему два читателя с одинаковыми данными остаются разными объектами?


Оформление результата

Сдайте пять файлов:

practice-01/
├── task_1.swift
├── task_2.swift
├── task_3.swift
├── task_4.swift
└── task_5.swift

Каждый файл должен самостоятельно запускаться и содержать:

  • реализацию класса;
  • не менее трёх содержательных assert;
  • хотя бы один пример успешной операции;
  • хотя бы одну проверку некорректных входных данных;
  • краткий комментарий с перечислением защищаемых инвариантов.

Критерии оценки

РазделДоля
Задания 1–2: базовые классы, свойства и static25%
Задание 3: вычисляемое свойство и валидация15%
Задание 4: инкапсуляция состояния счёта20%
Задание 5: комплексная модель и ссылочная семантика25%
Качество кода и проверки15%

Штрафы

  • прямое изменение состояния вместо использования публичных методов;
  • отсутствие проверки инвариантов;
  • использование ! там, где можно безопасно обработать optional;
  • один общий файл, который не компилируется или не запускается;
  • удаление проверок вместо исправления реализации.

Вопросы для самопроверки

  1. Чем класс отличается от экземпляра класса?
  2. Почему у свойства area нет отдельного хранимого значения?
  3. Чем private отличается от private(set)?
  4. Почему константа let member всё равно позволяет вызвать метод, изменяющий состояние экземпляра класса?
  5. В чём разница между свойством экземпляра value и свойством типа createdCount?
  6. Что должен делать проваливающийся инициализатор при нарушении инварианта?
  7. Зачем методы deposit(_:) и withdraw(_:) возвращают Bool?
  8. Что означает оператор ===?
  9. Почему две переменные после let second = first видят изменения одного объекта?
  10. В каком случае для модели лучше выбрать структуру, а не класс?

Итог

В этой работе вы проектируете не просто набор свойств, а объекты с контролируемым состоянием. Основной критерий хорошего решения: внешний код может выполнять разрешённые операции, но не способен обойти правила предметной области.