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

Что это за уровни?

Существует 5 уровней доступа: open, public, internal, fileprivate и private

Давай разберёмся подробнее, что это такое и как применять.

OPEN

Это как особая входная дверь в дом. Когда ты объявляешь что-то с уровнем доступа open это означает, что другие модули могут не только видеть и использовать этот код, но и наследовать и переопределять его.

Где может пригодиться?

  • Расширяемые Классы: Идеально подходит когда мы хотим создать класс, который другие разработчики могут наследовать и дополнять своими методами и свойствами. Создаем "родительский" класс с базовым функционалом, а другие классы могут строить на нем, как на фундаменте, добавляя свои индивидуальные детали.

  • Библиотеки и Фреймворки: Если мы пишем код, который будет использоваться в других проектах, уровень доступа open может быть очень полезным. Это позволяет разработчикам, использующим нашу библиотеку, расширять и адаптировать ее компоненты под свои потребности.

  • Сильная Связь: Когда мы хотим создать крепкую связь между разными классами и модулями, open может сыграть важную роль. Предоставление "открытого" интерфейса, который может быть наследован и расширен, способствует гибкости и взаимодействию.

// Открываем класс для наследования и расширения
open class Shape {
    open func area() -> Double {
        return 0.0
    }
}

// Cоздаем наследника в своем модуле
open class Circle: Shape {
    private var radius: Double

    public init(radius: Double) {
        self.radius = radius
    }

    // Переопределяем метод рассчета площади
    open override func area() -> Double {
        return Double.pi * radius * radius
    }
}

// Создаем наследника в другом модуле
class CustomShape: Shape {
    override func area() -> Double {
        return super.area() * 2
    }
}
/* Другой модуль может наследовать и расширять классы, но если метод родительского 
класса не будет помечен как open переопределить его мы не сможем */

PUBLIC

Элементы с этим уровнем видимости доступны для всех, но с небольшим нюансом. Это как реклама на большом щите - каждый, кто проходит мимо, может это видеть, но они не могут быть переопределены (для методов) или наследованы (для классов) в других модулях.

Где может пригодиться?

  • Базовые функциональности: Мы можем использовать public для методов или свойств, которые предоставляют базовую функциональность нашего модуля и которые мы не хотим, чтобы можно было изменять или расширять в других модулях. Это помогает поддерживать стабильный интерфейс и избегать непредвиденных изменений.

  • Утилиты и вспомогательные классы: Если модуль содержит набор утилит или вспомогательных классов, которые могут быть полезными для других модулей, но мы хотим предотвратить их подклассирование или переопределение - объявляем их с уровнем доступа public.

  • Создание API для чтения данных: Если мы создаем API для доступа к данным (база данных, кеш, хранилище) - используем public для предоставления доступа к этим данным без возможности изменения или переопределения методов работы с данными.

// Создаем класс для наследования и расширения
public class Shape {
    public func area() -> Double {
        return 0.0
    }
}

// Cоздаем наследника в своем модуле
class Circle: Shape {
    private var radius: Double

    public init(radius: Double) {
        self.radius = radius
    }

    // Переопределяем метод рассчета площади
    open override func area() -> Double {
        return Double.pi * radius * radius
    }
}

/* Создаем наследника в другом модуле - ошибка компиляции, вызванная нарушением правил уровней доступа.
   Cannot inherit from non-open class 'Shape' outside of its defining module
   Overriding non-open instance method outside of its defining module
*/
class CustomShape: Shape {
    public override func area() -> Double {
        return super.area() * 2
    }
}

На этом мы завершаем рассмотрение уровней дающих доступ внутри нашего программного пространства и за его пределами и переходим к тому, что позволяет нам контролировать связи исключительно внутри своего модуля.


INTERNAL

Уровень доступа по умолчанию. Элементы с этим уровнем доступа видны внутри всего "модуля". Модуль - это что-то вроде соседства в программировании где элементы делят один и тот же "двор". Другие модули извне не видят эти элементы, но все внутри модуля могут.

Где может пригодиться?

  • Внутренние реализационные детали: Если есть внутренние классы, структуры или функции, которые не должны быть видны извне, но играют важную роль внутри модуля, уровень доступа internal будет подходящим выбором.

  • Совместимость и расширяемость: Можно использовать для методов или свойств, которые должны быть доступны для других частей проекта, но не обязательно для внешних модулей. Это обеспечивает баланс между функциональностью и скрытностью.

// Создаем класс для наследования и расширения
internal class Shape {
    func area() -> Double {
        return 0.0
    }
}

// Cоздаем наследника в своем модуле
internal class Circle: Shape {
    private var radius: Double

    init(radius: Double) {
        self.radius = radius
    }

    // Переопределяем метод рассчета площади
    override func area() -> Double {
        return Double.pi * radius * radius
    }
}

// Другой класс внутри модуля может наследовать и расширять классы
class CustomShape: Shape {
    override func area() -> Double {
        return super.area() * 2
    }
}

FILEPRIVATE

Этот уровень "расширяет" видимость до всего файла .swift, где элемент создан. То есть, любой код в этом файле может иметь доступ к элементам с такой видимостью. Это как комната в доме, к которой есть доступ только у семьи, живущей в этом доме.

Где может пригодиться?

  • Инкапсуляция внутренних деталей: Мы можем использовать fileprivate, чтобы скрыть внутренние детали реализации от других файлов и модулей. Это помогает обеспечить более чистую и защищенную архитектуру кода.

  • Организация кода в файле: Внутри файла лучше использовать fileprivate, чтобы явно указать какие части кода предназначены для внутреннего использования друг другом, но не видны за пределами файла.

  • Реализация расширений: Когда мы расширяем функциональность классов или структур хорошим выбором будет использовать fileprivate для методов или свойств, которые должны быть доступны только в пределах этого файла, но не извне.

// Создаем класс для наследования и расширения
fileprivate class Shape {
    func area() -> Double {
        return 0.0
    }
}

// Cоздаем наследника внутри того же файла .swift
fileprivate class Circle: Shape {
    private var radius: Double

    init(radius: Double) {
        self.radius = radius
    }
  
    // Переопределяем метод рассчета площади
    override fileprivate func area() -> Double {
        return Double.pi * radius * radius
    }
}

/* Важно: Наследник обязательно должен быть помечен как private или fileprivate 
т.к не может иметь более высокий уровень доступа, чем его родитель. Это связано с 
тем, что подкласс может получить доступ к членам суперкласса, и если бы уровень 
доступа подкласса был более высоким, это могло бы привести к 
утечке информации из модуля с более ограниченным уровнем доступа. */

PRIVATE

Самый закрытый уровень. Элементы с таким уровнем видимости доступны только внутри того файла где они созданы. Это подобно защищенному хранилищу доступ к которому имеет лишь один человек. Однако в расширении данного типа (класса, структуры или перечисления) есть попытка получить доступ к private элементам этого типа. Это позволяет расширить функциональность типа используя private элементы, которые в противном случае не видны за пределами этого файла.

Где может пригодиться?

  • Тайные Детали: Используй private, когда хочешь создать внутренние функции, переменные или свойства, которые должны оставаться невидимыми для всех остальных частей кода. Это улучшает безопасность, предотвращая нежелательное вмешательство.

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

  • Скрытое расширение: Может быть очень удобным, когда мы хотим добавить дополнительную функциональность типу, не раскрывая все детали внешнему миру.

// Создаем класс для наследования и расширения
private class Shape {

     private func area() -> Double {
        return 0.0
    }

     func printInfo() {
        print("Some shape")
    }
}


// Cоздаем наследника внутри того же файла .swift
private class Circle: Shape {
    private var radius: Double

    init(radius: Double) {
        self.radius = radius
    }

    // Ошибка компиляции, переопределение private метода запрещено
   // Method does not override any method from its superclass
    override func area() -> Double {
        return Double.pi * radius * radius
    }
    
    // Переопределяем метод т.к он не помечен как private
    override func printInfo() {
        print("Circle")
    }
}


// Создаем расширение внутри того же файла .swift
extension Shape {
    func calculateArea() -> Double {
        return area() // Обращение к приватному методу area() внутри расширения
    }
}

Заключение

Ну вот мы и подошли к финишу, надеюсь, теперь у тебя есть ясное представление о том, как уровни доступа могут сделать твой код более структурированным и надежным. Помни, когда ты выбираешь уровень доступа, словно создаешь правила игры в своем собственном кодовом мире – решай, кто получит доступ, а кто останется за дверью. Всегда держи в уме основные принципы: защита данных, инкапсуляция, поддержание порядка и безопасности. Не забывай воплощать эти знания в своей будущей практике программирования.

Комментарии (8)


  1. Rusrst
    20.08.2023 15:52

    Больше интересно как вы там без try catch с runtime error живёте :) и что делаете, если у вас у вас есть дубликат в diffable data source? :)


    1. debug45
      20.08.2023 15:52

      try/catch в Swift очень даже есть


      1. Rusrst
        20.08.2023 15:52

        Но не для runtime exception, если функция не выбрасывает исключение, перехватить его нельзя. Как раз если diffable data source имеет duplicate, то перехватить его нельзя, будет краш в runtime, так себе try catch, не находите?


        1. nikita_dol
          20.08.2023 15:52
          +2

          Это достаточно обычная проблема, когда в Swift используется что-то из Objective-C. Если какой-то Objective-C код падает в рантайме, то можно обернуть его в другой Objective-C код, который это отловит. Тыц

          Или от меня скриншот


          1. Rusrst
            20.08.2023 15:52

            Спасибо, я не swift разраб, этого не знал, но это же не swift по идее, так что конечно спорно, но спасибо!


            1. nikita_dol
              20.08.2023 15:52
              +2

              Сам враппер пишется на Objective-C, но его можно использовать внутри Swift.
              Например, у меня на скриншоте с кодом на Swift в 56 строке используется tryBlock из Objective-C. А сам код Objective-C со Stack Overflow. Поэтому, мы можем писать врапперы для Objective-C в Objective-C и потом использовать в Swift


              1. Rusrst
                20.08.2023 15:52

                Прикольно, у нас таких проблем не стыке java и kotlin (за исключением null) нет, поэтому выглядит слегка непривычно. Ещё раз спасибо! :)


  1. Maksclub
    20.08.2023 15:52

    internal прямо хороший модификатор организовывать пакеты, и не раскрывать классы как внутреннюю часть пакета