Часто я встречаю разработчиков, которые пишут код на объектно-ориентированном языке программирования, но не понимают принципов ООП. Это могут быть начинающие девелоперы, которые еще на собеседованиях сталкиваются с проблемами объяснения принципов. А также это могут быть, казалось бы, опытные программисты, которые не понимают принципов, заложенных в язык программирования, на котором они пишут. Второй случай хотелось бы встречать реже, но на практике это не так. Часто разработчики смотрят на наследование или полиморфизм, как на особенности языка, как на какой-то технический инструмент и не думают, о вещах, которые лежат в основе этих механизмов.
Все, что будет изложено ниже — сугубо мои размышления, я не претендую на статус истины в последней инстанции и не жду, что все примут мою точку зрения. Я надеюсь, эта статья натолкнет на размышления и даст толчок к развитию собственного понимания у каждого читателя.
Примеры кода буду приводить из iOS разработки.
Я считаю, если ты пишешь код на объектно-ориентированном языке программирования, ты обязан не только знать определения, но и понимать суть, которая вложена в эту парадигму.
Также заранее хочу добавить, это описание идеального сферического программирования в вакууме и в реальности множество вещей нарушаются в угоду практичности. Но стремление к идеалу только улучшит качество кода. И не стоит забывать, что гонка за крайностью — тоже плохо.
Это и есть принцип наследования, где каждый админ/VIP-клиент/аноним являются пользователями, но не каждый пользователь должен быть админом или VIP-пользователем.
/**
Нужно сделать экран профиля пользователя, в котором отображаются имя и фамилия. Этот экран должен переиспользоваться.
*/
// INCORRECT
class UserProfileViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet private var firstNameLabel: UILabel!
@IBOutlet private var lastNameLabel: UILabel!
/**
Заполнение данных оставляем классу наследнику в виде абстрактных методов
*/
// MARK: - Abstract methods
func firstName() -> String? {
return nil
}
func lastName() -> String? {
return nil
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
firstNameLabel.text = firstName()
lastNameLabel.text = lastName()
}
}
/**
Создаем класс-наследник, который отвечает за функцию заполнения данных. В таком случае, наследник будет выполнять роль не только UIViewController, а и роль модели, которая предоставляет данные для отображения.
*/
class UserProfileModel: UserProfileViewController {
override func firstName() -> String? {
return "Name"
}
override func lastName() -> String? {
return "Last name"
}
}
// CORRECT
/**
Корректней будет, добавить новый класс-модель, которая будет предоставлять данные.
*/
class UserProfileModel {
func firstName() -> String? {
return "Name"
}
func lastName() -> String? {
return "Last name"
}
}
/**
В таком случае у нас будут два отдельных класса, каждый из которых имеет свою зону ответственности.
*/
class UserProfileViewController: UIViewController {
var model: UserProfileModel?
// MARK: - IBOutlets
@IBOutlet private var firstNameLabel: UILabel!
@IBOutlet private var lastNameLabel: UILabel!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUserInfo()
}
// MARK: - Private
private func setupUserInfo() {
firstNameLabel.text = model?.firstName()
lastNameLabel.text = model?.lastName()
}
}
// PERFECT
/**
Еще лучше, взаимодействие между двумя типами классов, контроллер и модель, сделать через протокол, чтобы можно было создавать и использовать разные модели.
*/
protocol UserProfileProtocol {
func firstName() -> String?
func lastName() -> String?
}
class UserProfileViewController: UIViewController {
var model: UserProfileProtocol?
// MARK: - IBOutlets
@IBOutlet private var firstNameLabel: UILabel!
@IBOutlet private var lastNameLabel: UILabel!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUserInfo()
}
// MARK: - Private
private func setupUserInfo() {
firstNameLabel.text = model?.firstName()
lastNameLabel.text = model?.lastName()
}
}
class UserProfileModel: UserProfileProtocol {
// MARK: - UserProfileProtocol
func firstName() -> String? {
return "Name"
}
func lastName() -> String? {
return "Last name"
}
}
Это пример, когда непонимание принципа наследования приводит к сложности понимания системы.
Абстракция гласит — останавливаем внимание на важных и необходимых аспектах объекта и игнорируем ненужные для нас.
/**
Распространенная ситуация: создаем класс, к примеру, простую модель пользователя. Но с добавлением функционала, все больше появляется полей и методов в этом классе.
*/
// INCORRECT
class User {
let firstName: String
let lastName: String
let fullName: String
let age: Int
let birthday: Date
let street: String
let postalCode: Int
let city: String
var phoneNumber: String?
var phoneCode: String?
var phoneFlag: UIImage?
var isLoggined: Bool = false
var isAdmin: Bool = false
// MARK: - Init
init(firstName: String,
lastName: String,
fullName: String,
age: Int,
birthday: Date,
street: String,
postalCode: Int,
city: String) {
self.firstName = firstName
self.lastName = lastName
self.fullName = fullName
self.age = age
self.birthday = birthday
self.street = street
self.postalCode = postalCode
self.city = city
}
// MARK: - Admin functionality
func createNewReport() {
guard isAdmin else { return }
print("New report created")
}
func updateReport(for user: User) {
guard isAdmin else { return }
print("Update report for \(user.fullName)")
}
}
// CORRECT
/**
Правильней будет, декомпозировать код, абстрагируя части большого сложного класса на маленькие компоненты.
*/
class Address {
let street: String
let postalCode: Int
let city: String
init(street: String,
postalCode: Int,
city: String) {
self.street = street
self.postalCode = postalCode
self.city = city
}
}
class Name {
let firstName: String
let lastName: String
init(firstName: String,
lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
var fullName: String {
firstName + " " + lastName
}
}
class PhoneNumber {
let phone: String
let code: String
let flag: UIImage
init(phone: String,
code: String,
flag: UIImage) {
self.phone = phone
self.code = code
self.flag = flag
}
}
class User {
/**
В результате, класс User уменьшился в размерах, при этом мы абстрагируемся от деталей имени и адреса.
*/
let name: Name
let address: Address
let birthday: Date
var phoneNumber: PhoneNumber?
init(name: Name,
address: Address,
birthday: Date) {
self.name = name
self.address = address
self.birthday = birthday
}
}
/**
Так как после логина система получает залогиненого Пользователя, то класс User не должен отвечать за состояния системы. За статус логина будет отвечать новая сущность, тем самым система абстрагируется от деталей логики этого статуса.
*/
class LoginSession {
var user: User?
var isLoggined: Bool {
user != nil
}
}
/**
Дополнительные свойства Администратора выносяться в класс-наследник Пользователя.
*/
class Admin: User {
func createNewReport() {
print("New report created")
}
func updateReport(for user: User) {
print("Update report for \(user.fullName)")
}
}
Полиморфизм — когда наследники делают все по своему, но результат работы такой же как у базового класса. Если наследник занимается чем-то своим и не дает результат такой, который ожидается от базового класса — значит с наследованием и полиморфизмом что-то не так.
Добавлю, что полиморфизм тесно связан с наследованием и проблемы в наследовании отзываются еще большими проблемами в полиморфизме.
/**
Создаем кнопку, у которой при нажатии цвет бэкграунда устанавливается в оригинальный цвет но с альфаканалом 0,5
*/
// INCORRECT
class Button: UIButton {
/**
Добавляем два метода, которые устанавливают цвет бекграунда для состояния нажатой кнопки и нормального состояния кнопки
*/
func decorateSelected() {
backgroundColor = backgroundColor?.withAlphaComponent(0.5)
}
func decorateDeselected() {
backgroundColor = backgroundColor?.withAlphaComponent(1)
}
override var isSelected: Bool {
didSet {
if isSelected {
decorateSelected()
} else {
decorateDeselected()
}
}
}
}
// SAMPLE
/**
Проблемой будет то, что методы, декорирующие кнопку в разных состояних, являются публичными. А это значит, что можно нарушить логику работы кнопки, вызвав метод в неправильный момент.
*/
let button = Button()
button.decorateSelected()
// CORRECT
class Button: UIButton {
override var isSelected: Bool {
didSet {
if isSelected {
decorateSelected()
} else {
decorateDeselected()
}
}
}
/**
Мы сделали методы, настраивающие внешний вид кнопки, приватными, тем самым обеспечили правильную логику отображения.
*/
// MARK: - Private
private func decorateSelected() {
backgroundColor = backgroundColor?.withAlphaComponent(0.5)
}
private func decorateDeselected() {
backgroundColor = backgroundColor?.withAlphaComponent(1)
}
}
// PERFECT
/**
Но! У кнопки остаеться возможнось измененить цвет через базовое поле var backgroundColor: UIColor?. Поэтому, немного заморочившись, делаем невозможным менять цвет в момент, когда кнопка нажата.
*/
class Button: UIButton {
override var backgroundColor: UIColor? {
get {
super.backgroundColor
}
set {
if isHighlighted == false {
super.backgroundColor = newValue
}
}
}
override var isHighlighted: Bool {
willSet {
if newValue {
decorateSelected()
}
}
didSet {
if isHighlighted == false {
decorateDeselected()
}
}
}
// MARK: - Private
private func decorateSelected() {
backgroundColor = backgroundColor?.withAlphaComponent(0.5)
}
private func decorateDeselected() {
backgroundColor = backgroundColor?.withAlphaComponent(1)
}
}
Инкапсуляция — не бездумное сокрытие каких-то полей или методов, а проектирование класса с определенным набором возможностей, которые не должны нарушаться.
Если просуммировать, то инкапсуляция — проектирование самостоятельной единицы (объекта), которая выполняет некую роль в системе, имеет набор параметров и методов. При этом все, что может нарушить роль этого объекта, скрыто.
К сожалению, не доступен сервер mySQL