[Программирование, Java, Совершенный код, Проектирование и рефакторинг, Kotlin] Свойства против методов
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
На первый взгляд, такой вопрос как выбор свойства или метода кажется простым. Но это до тех пор, пока вы не столкнётесь с непониманием в своей команде. Хотя есть устоявшиеся практики, их формулировки достаточно расплывчаты. В такого рода вопросах есть некоторая степень свободы, которая затрудняет наш выбор, а кажущаяся простота даёт плодородную почву для споров.
Бэкграунд Java-программистов
Язык программирования — это основной инструмент программиста. Наличие или отсутствие каких-либо конструкций формирует определённый стиль кодирования. Вот, например, в Java нет свойств, есть только поля и методы.
Возьмём для примера следующий класс:
public class Point {
public double x;
public double y;
}
Мы имеем класс, описывающий точку на плоскости. Что с ним не так? Во-первых, так как это открытые поля, то они доступны для редактирования извне. Во-вторых, мы открываем детали реализации, что храним точку в декартовых координатах.
Поэтому, обычно, так не пишут, а инкапсулируют поля за геттерами и сеттерами:
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
}
Стандартная практика на Java, в IDE даже есть специальные генераторы для этого.
Мы убрали прямой доступ к полям с помощью методов get и set.
Хотя они выглядят как функции, в сущности, являются геттерами и сеттерами.
Сахар в Котлине
В Котлине у нас есть свойства и мы можем не писать простыню из методов get и set:
class Point(var x: Double, var y: Double)
Выглядит лаконично, не так ли? Обращение к свойству тоже удобно: x вместо getX().
При необходимости мы можем переопределить геттер или сеттер:
var x: Double = 0
set(value) {
if (value >= 0) field = value
}
В Котлине мы всегда имеем дело со свойствами, поля скрыты за сеттерами и геттерами. То есть мы имеем все преимущества методов, но при этом можем обращаться как с полями.
Нам подвезли сахар, но старые привычки остались. Я часто замечаю, что программисты на Java продолжают писать методы get и set в классах на Котлине, когда это уже необязательно. Название самих методов говорит о том, что их можно сконвертировать в свойства. Но всегда ли нужно предпочесть свойство методу? Если у нас есть функция без параметров, то выбор не всегда очевиден.
Общепринятые соглашения
Официальная документация говорит нам, что функции без параметров могут быть взаимозаменяемы read-only свойствами.
Ниже дан алгоритм, по которому можно определить, когда предпочесть свойство методу:
- если свойство не бросает исключение (exception)
- дёшево для вычисления (или можно закешировать при первом запуске)
- возвращает одно и то же значение при каждом вызове, если состояние объекта не изменилось
Я ожидал увидеть более развёрнутое руководство. Для первого пункта я бы добавил про любой сайд-эффект, который может происходить при вызове функции. С последним пунктом тоже не всё так просто.
Например, если у нас есть класс User:
class User(
val firstName: String,
val lastName: String
)
Нужно ли полное имя делать свойством или всё же методом?
val fullName get() = "$firstName $lastName"
В данном случае создаётся всегда новый объект String, хотя это деталь реализации. Но значения всегда будут равны при сравнении через equals. На крайний случай можно закешировать fullName, пожертвовав памятью.
Но вот пункт про вычислимость вызывает больше всего вопросов.
Сложно или легко вычислимые свойства
Кажется, это довольно расплывчатое требование. Что значит сложно вычислимое? Если подразумеваются тяжёлые вычисления, такие как запрос в сеть или к БД, то мы должны будем вынести вызов в отдельный поток. В этом случае асинхронный вызов будет выглядеть по-другому: метод с коллбэком, реактивный поток или корутина. Но речь, скорее всего, не об этом.
Рассмотрим следующий пример:
class DocumentModel {
val activePageIndex: Int
}
У нас есть класс модели документа, у которого есть свойство activePageIndex, которое возвращает текущий индекс страницы. Мы не знаем внутренней реализации, но предполагаем, что это свойство и согласно принятому соглашению, мы можем не беспокоиться о производительности и спокойно использовать его в цикле:
images.forEach { image ->
document addImage(image, document.activePageIndex)
}
Допустим, чтобы получить текущую страницу, нужно пробежаться по всему документу, то есть сделать некоторые вычисления. В этом случае оптимально сохранить текущую страницу в переменную, прежде чем использовать её в цикле:
val pageIndex = document.activePageIndex
images.forEach { image ->
document addImage(image, pageIndex)
}
Но чтобы это понять, нужно заглянуть в реализацию. Когда программист видит свойство, то он делает некоторые допущения в использовании, полагая, что автор класса позаботился о принятом соглашении. В этом случае автор допустил небрежность, чем ввёл в заблуждение. По хорошему, нужно сделать метод вместо свойства для получения активной страницы и назвать её как-то по-другому, например, findActivePageIndex.
Интерфейс важнее реализации
С одной стороны, приведённый выше пример показывает, насколько важно думать об интерфейсе, как он будет использоваться на клиентской стороне. С другой стороны, реализация накладывает ограничение на интерфейс. Если сложные вычисления, то нужно использовать функцию вместо свойства. Здесь мы вступаем в некоторое противоречие, что первичнее, реализация или интерфейс? Мы заранее не можем сказать об эффективности и есть соблазн впасть в крайность — всегда делать методы в интерфейсе. Особенно после Java непривычно видеть свойства в интерфейсе. При этом, метод, начинающийся со слова get или set никого не смущает.
При проектировании интерфейса, по-моему, мы должны в первую очередь думать о клиентском коде. Если мы будем ставить реализацию впереди интерфейса, то мы получим плохой API класса. Цена такой ошибки может быть рефакторинг всех вызовов в проекте. Разработчики библиотек хорошо это понимают, когда изменение API ломает сторонний код или меняет поведение класса, на которое клиент не рассчитывал.
Частая ситуация, что интерфейс создают при помощи рефакторинга в IDE, извлекая из существующего класса. Похоже, что это порочная практика. В результате такого рефакторинга на выходе мы получаем кашу из методов, зачастую несвязанных между собой.
Интерфейс — это так же важно, как и подбор подходящего имени для переменной или функции. Это контракт между автором и пользователем интерфейса. Реализаций может быть множество, включая самые неоптимальные. Это уже детали. При реализации свойств мы должны позаботиться, чтобы оно соответствовало принятому соглашению о лёгкой вычислимости.
Фундаментальные отличия
Хорошо, предположим, мы принимаем, что интерфейс первичен по отношению к реализации. Но из каких соображений исходить при его проектировании. В каком случае выбрать свойство, а когда нужен метод?
У свойств и методов есть более глубокие различия. Когда мы проектируем класс, то его можно разделить на две условные части:
- Состояние. Можно рассматривать как данные, которые описывают характеристики или черты объекта. В этом случае больше подходят свойства.
- Поведение. То есть то, что можно сделать с объектом. За это отвечают методы.
Они обычно изменяют состояние.
Это довольно простое правило, которое поможет при выборе свойства или метода.
Методы принято начинать с глагола и если вы не можете подобрать ничего лучше, чем get/set, то это явный признак свойства.
Вместо заключения
Разберём первоначальный пример, только сделаем его интерфейсом:
interface Point {
var x: Double
var y: Double
}
Так что с ним не так?
Во-первых, два отдельных сеттера для координат x и y. Когда мы определяем точку в пространстве, то мы задаём их в паре, то есть атомарно. Меняя их независимо, мы создаём возможность для ошибок.
Добавим метод для задания координат и сделаем координаты x и y только для чтения:
interface Point {
val x: Double
val y: Double
fun setCoordinates(x: Double, y: Double)
}
Во-вторых, интерфейс недостаточно гибкий. Иногда удобно работать с полярными координатами, но в интерфейсе только прямоугольные. Таким образом, мы неявно раскрываем реализацию.
Расширим интерфейс:
interface Point {
val x: Double
val y: Double
val radius: Double
val angle: Double
fun setCartesian(x: Double, y: Double)
fun setPolar(radius: Double, angle: Double)
}
Как мы видим, хороший интерфейс спроектировать не так просто. Хотя можно было бы ограничиться data-классом:
data class Point(
var x: Double,
var y: Double
)
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, Социальные сети и сообщества] Как мы запускали ещё один подкаст для программистов
- [Программирование микроконтроллеров, Процессоры, Игры и игровые приставки] Doom запустили на ПЛИС iCE40
- [Open source, Программирование, Java, Kotlin] Jmix / CUBA Platform: итоги 2020 и планы на 2021 год
- [Разработка веб-сайтов, JavaScript, Nginx] Солидные фронтенды: конфигурация
- [Информационная безопасность, JavaScript, История IT, Софт, IT-компании] Новая фишинговая атака использует азбуку Морзе для сокрытия вредоносных URL-адресов
- [Программирование, Алгоритмы, Go] Algorithms in Go: Dutch National Flag
- [Программирование, GTD] Что послушать, когда пишешь код: бесплатные миксы, заглушка для второго монитора и эмбиент-плеер
- [Программирование, Assembler] Перевод числа в строку с помощью FPU
- [Разработка веб-сайтов, PHP, Программирование] Enum в PHP 8.1 — для чего нужен enum, и как реализован в PHP
- [Разработка веб-сайтов, Программирование] Custom Elements из Angular в Angular
Теги для поиска: #_programmirovanie (Программирование), #_java, #_sovershennyj_kod (Совершенный код), #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_kotlin, #_properties_vs_functions, #_kotlin, #_java, #_val, #_fun, #_programmirovanie (
Программирование
), #_java, #_sovershennyj_kod (
Совершенный код
), #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 00:01
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
На первый взгляд, такой вопрос как выбор свойства или метода кажется простым. Но это до тех пор, пока вы не столкнётесь с непониманием в своей команде. Хотя есть устоявшиеся практики, их формулировки достаточно расплывчаты. В такого рода вопросах есть некоторая степень свободы, которая затрудняет наш выбор, а кажущаяся простота даёт плодородную почву для споров. Бэкграунд Java-программистов Язык программирования — это основной инструмент программиста. Наличие или отсутствие каких-либо конструкций формирует определённый стиль кодирования. Вот, например, в Java нет свойств, есть только поля и методы. Возьмём для примера следующий класс: public class Point {
public double x; public double y; } Мы имеем класс, описывающий точку на плоскости. Что с ним не так? Во-первых, так как это открытые поля, то они доступны для редактирования извне. Во-вторых, мы открываем детали реализации, что храним точку в декартовых координатах. Поэтому, обычно, так не пишут, а инкапсулируют поля за геттерами и сеттерами: public class Point {
private double x; private double y; public Point(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public void setX(double x) { this.x = x; } public double getY() { return y; } public void setY(double y) { this.y = y; } } Стандартная практика на Java, в IDE даже есть специальные генераторы для этого. Мы убрали прямой доступ к полям с помощью методов get и set. Хотя они выглядят как функции, в сущности, являются геттерами и сеттерами. Сахар в Котлине В Котлине у нас есть свойства и мы можем не писать простыню из методов get и set: class Point(var x: Double, var y: Double)
Выглядит лаконично, не так ли? Обращение к свойству тоже удобно: x вместо getX(). При необходимости мы можем переопределить геттер или сеттер: var x: Double = 0
set(value) { if (value >= 0) field = value } В Котлине мы всегда имеем дело со свойствами, поля скрыты за сеттерами и геттерами. То есть мы имеем все преимущества методов, но при этом можем обращаться как с полями. Нам подвезли сахар, но старые привычки остались. Я часто замечаю, что программисты на Java продолжают писать методы get и set в классах на Котлине, когда это уже необязательно. Название самих методов говорит о том, что их можно сконвертировать в свойства. Но всегда ли нужно предпочесть свойство методу? Если у нас есть функция без параметров, то выбор не всегда очевиден. Общепринятые соглашения Официальная документация говорит нам, что функции без параметров могут быть взаимозаменяемы read-only свойствами. Ниже дан алгоритм, по которому можно определить, когда предпочесть свойство методу:
Я ожидал увидеть более развёрнутое руководство. Для первого пункта я бы добавил про любой сайд-эффект, который может происходить при вызове функции. С последним пунктом тоже не всё так просто. Например, если у нас есть класс User: class User(
val firstName: String, val lastName: String ) Нужно ли полное имя делать свойством или всё же методом? val fullName get() = "$firstName $lastName"
В данном случае создаётся всегда новый объект String, хотя это деталь реализации. Но значения всегда будут равны при сравнении через equals. На крайний случай можно закешировать fullName, пожертвовав памятью. Но вот пункт про вычислимость вызывает больше всего вопросов. Сложно или легко вычислимые свойства Кажется, это довольно расплывчатое требование. Что значит сложно вычислимое? Если подразумеваются тяжёлые вычисления, такие как запрос в сеть или к БД, то мы должны будем вынести вызов в отдельный поток. В этом случае асинхронный вызов будет выглядеть по-другому: метод с коллбэком, реактивный поток или корутина. Но речь, скорее всего, не об этом. Рассмотрим следующий пример: class DocumentModel {
val activePageIndex: Int } У нас есть класс модели документа, у которого есть свойство activePageIndex, которое возвращает текущий индекс страницы. Мы не знаем внутренней реализации, но предполагаем, что это свойство и согласно принятому соглашению, мы можем не беспокоиться о производительности и спокойно использовать его в цикле: images.forEach { image ->
document addImage(image, document.activePageIndex) } Допустим, чтобы получить текущую страницу, нужно пробежаться по всему документу, то есть сделать некоторые вычисления. В этом случае оптимально сохранить текущую страницу в переменную, прежде чем использовать её в цикле: val pageIndex = document.activePageIndex
images.forEach { image -> document addImage(image, pageIndex) } Но чтобы это понять, нужно заглянуть в реализацию. Когда программист видит свойство, то он делает некоторые допущения в использовании, полагая, что автор класса позаботился о принятом соглашении. В этом случае автор допустил небрежность, чем ввёл в заблуждение. По хорошему, нужно сделать метод вместо свойства для получения активной страницы и назвать её как-то по-другому, например, findActivePageIndex. Интерфейс важнее реализации С одной стороны, приведённый выше пример показывает, насколько важно думать об интерфейсе, как он будет использоваться на клиентской стороне. С другой стороны, реализация накладывает ограничение на интерфейс. Если сложные вычисления, то нужно использовать функцию вместо свойства. Здесь мы вступаем в некоторое противоречие, что первичнее, реализация или интерфейс? Мы заранее не можем сказать об эффективности и есть соблазн впасть в крайность — всегда делать методы в интерфейсе. Особенно после Java непривычно видеть свойства в интерфейсе. При этом, метод, начинающийся со слова get или set никого не смущает. При проектировании интерфейса, по-моему, мы должны в первую очередь думать о клиентском коде. Если мы будем ставить реализацию впереди интерфейса, то мы получим плохой API класса. Цена такой ошибки может быть рефакторинг всех вызовов в проекте. Разработчики библиотек хорошо это понимают, когда изменение API ломает сторонний код или меняет поведение класса, на которое клиент не рассчитывал. Частая ситуация, что интерфейс создают при помощи рефакторинга в IDE, извлекая из существующего класса. Похоже, что это порочная практика. В результате такого рефакторинга на выходе мы получаем кашу из методов, зачастую несвязанных между собой. Интерфейс — это так же важно, как и подбор подходящего имени для переменной или функции. Это контракт между автором и пользователем интерфейса. Реализаций может быть множество, включая самые неоптимальные. Это уже детали. При реализации свойств мы должны позаботиться, чтобы оно соответствовало принятому соглашению о лёгкой вычислимости. Фундаментальные отличия Хорошо, предположим, мы принимаем, что интерфейс первичен по отношению к реализации. Но из каких соображений исходить при его проектировании. В каком случае выбрать свойство, а когда нужен метод? У свойств и методов есть более глубокие различия. Когда мы проектируем класс, то его можно разделить на две условные части:
Это довольно простое правило, которое поможет при выборе свойства или метода. Методы принято начинать с глагола и если вы не можете подобрать ничего лучше, чем get/set, то это явный признак свойства.
Вместо заключения Разберём первоначальный пример, только сделаем его интерфейсом: interface Point {
var x: Double var y: Double } Так что с ним не так? Во-первых, два отдельных сеттера для координат x и y. Когда мы определяем точку в пространстве, то мы задаём их в паре, то есть атомарно. Меняя их независимо, мы создаём возможность для ошибок. Добавим метод для задания координат и сделаем координаты x и y только для чтения: interface Point {
val x: Double val y: Double fun setCoordinates(x: Double, y: Double) } Во-вторых, интерфейс недостаточно гибкий. Иногда удобно работать с полярными координатами, но в интерфейсе только прямоугольные. Таким образом, мы неявно раскрываем реализацию. Расширим интерфейс: interface Point {
val x: Double val y: Double val radius: Double val angle: Double fun setCartesian(x: Double, y: Double) fun setPolar(radius: Double, angle: Double) } Как мы видим, хороший интерфейс спроектировать не так просто. Хотя можно было бы ограничиться data-классом: data class Point(
var x: Double, var y: Double ) =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_java, #_sovershennyj_kod ( Совершенный код ), #_proektirovanie_i_refaktoring ( Проектирование и рефакторинг ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 00:01
Часовой пояс: UTC + 5