Практика 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.
Общие требования
- Все классы, от которых не требуется наследование, объявляйте как
final. - Не открывайте изменение состояния без необходимости. Используйте
let,privateиprivate(set). - Проверяйте инварианты в инициализаторах и методах.
- Не используйте принудительное извлечение optional через
!в основной логике программы. - После каждого задания добавьте проверки с помощью
assertи несколько понятных вызововprint. - Названия типов пишите в
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 = 32assert(temperature?.celsius == 0)
let oldValue = temperature?.celsiustemperature?.fahrenheit = -500assert(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 = memberlet 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: базовые классы, свойства и static | 25% |
| Задание 3: вычисляемое свойство и валидация | 15% |
| Задание 4: инкапсуляция состояния счёта | 20% |
| Задание 5: комплексная модель и ссылочная семантика | 25% |
| Качество кода и проверки | 15% |
Штрафы
- прямое изменение состояния вместо использования публичных методов;
- отсутствие проверки инвариантов;
- использование
!там, где можно безопасно обработать optional; - один общий файл, который не компилируется или не запускается;
- удаление проверок вместо исправления реализации.
Вопросы для самопроверки
- Чем класс отличается от экземпляра класса?
- Почему у свойства
areaнет отдельного хранимого значения? - Чем
privateотличается отprivate(set)? - Почему константа
let memberвсё равно позволяет вызвать метод, изменяющий состояние экземпляра класса? - В чём разница между свойством экземпляра
valueи свойством типаcreatedCount? - Что должен делать проваливающийся инициализатор при нарушении инварианта?
- Зачем методы
deposit(_:)иwithdraw(_:)возвращаютBool? - Что означает оператор
===? - Почему две переменные после
let second = firstвидят изменения одного объекта? - В каком случае для модели лучше выбрать структуру, а не класс?
Итог
В этой работе вы проектируете не просто набор свойств, а объекты с контролируемым состоянием. Основной критерий хорошего решения: внешний код может выполнять разрешённые операции, но не способен обойти правила предметной области.