Почему комбинирование получателей, сеттеров и модификатора частного доступа недостаточно для скрытия реализации?
В Очистить код :
public class Point {
public double x;
public double y;
}
Автор написал об этом классе Point()
:
Эта предоставляет реализацию. Действительно, это приведет к даже если переменные были private , и мы использовали одну переменную getters и сеттеры .
Это означает, что я должен записать свои школьные тетради.
Почему не объединяет геттеры, сеттеры и модификатор частного доступа?
8 ответов
Приведенный пример класса Point
с двумя открытыми членами типа double
с именем x
и y
, раскрывает его реализацию, позволяя любому, у кого есть объект типа Point
, чтобы узнать о его реализации. В этом случае раскрывается вся реализация.
Если вы изменили публичные члены на частные и создали пару геттеров и сеттеров, вы все равно выставляете свою реализацию. Если вы посмотрите на интерфейс объекта и увидите, что он имеет такие методы, как double getX()
, double getY()
, setX(double x)
и setY(double y)
, тогда вы не будете сидеть там, думая: «Ну, интересно, как они это реализовали. Я не могу сказать».
Вторая версия Point
на самом деле не отличается от первой, за исключением того, что на большинстве языков у нее будет много ненужный шаблон. В обоих случаях, глядя на интерфейс объекта, вы узнаете, как он реализован.
Если мы хотим изменить внутреннее представление Point
в полярные координаты, мы столкнемся с проблемами. Мы все еще можем использовать старый интерфейс, но он неуклюж. Вызов setX()
должен сделать внутреннее преобразование в прямоугольные координаты, изменить x, а затем преобразовать обратно в полярное, ввести ненужные ошибки округления и дополнительные вычисления.
Если бы мы начали с разработки интерфейса для Point
без ссылки на реализацию, у нас не было бы такого типа проблема. Скажем, мы решили, что точка имеет координату x и y, а также угол и величину. Мы могли бы иметь double x()
, double y()
, ---- +: = 15 =: + ---- и double angle()
. Мы могли бы сделать их неизменными, но предположим, что мы этого не сделали. Поэтому нам нужно double magnitude()
и rectangular(double x, double y)
для перемещения точка.
Что мы знаем о реализации? Ну, мы что-то знаем, поскольку двумерная точка - очень простая вещь, но мы не знаем, находится ли внутреннее представление объекта в полярных или прямоугольных координатах. Внутреннее представление может включать даже полярные и прямоугольные координаты, либо с булевыми, чтобы отслеживать, обновляется ли конкретный набор координат, либо заставляет все всегда обновляться автоматически.
Итак, в этом случае реализация не отображается, так как мы можем только догадываться, как она была реализована.
Объектно-ориентированная абстракция данных - это о поведенческой абстракции . Детали реализации скрыты за поведенческим интерфейсом, то есть интерфейсом, который делает что-то .
Переменная - это не поведение, это состояние. Геттер или сеттер - это просто переопределенная переменная.
Вопрос: «что делает Point
do ", not «какое у него состояние».
Например, как бы вы вычислили новую точку из существующей точки? У вас нет другого выбора, кроме как «разобрать» точку, выполнить все необходимые вычисления самостоятельно, а затем снова вернуть точку. И вы должны сделать это в каждый фрагмент кода , который хочет каким-то образом создать новые точки из старых. И что произойдет, если вы решите, что представление точек в виде декартовых координат неэффективно, и вы предпочтете представить его с помощью полярных координат?
Но что, если точка была не просто «мертвым» мешком данных? Что, если у этой точки было поведение? Что, если он знал сам , как построить новую точку из себя? Например, мы можем получить новый Point
, добавив вектор перемещения. Итак, мы добавляем метод add
к точке, которая принимает вектор перемещения в качестве параметра (пример в Scala, но это не имеет значения ):
class Point(val x: Double, val y: Double) {
def +(v: Vector) = ???
}
Теперь любая часть кода может создать новую точку, взяв существующую точку и добавив к ней вектор смещения, не зная что-либо о том, как Point
s или Vector
. Вы можете свободно изменять реализацию и представление одного или обоих, не влияя на любой другой код.
Если вы знаете немного Scala, возможно, вы заметили, что Scala генерировал автоматические getters для x
и y
. Я чувствую, что все в порядке, потому что вы иногда do хотите работать с ними отдельно. Однако я сделал not создание сеттеров!
Если мы возьмем что-то более сложное, как интерфейс коллекции, оно станет еще более очевидным. Как вы предпочитаете перемещаться по связанному списку:
// without abstraction:
var node = list.head
while (node != null) {
println(node)
node = node.next
}
// with abstraction:
list.foreach(println)
Совместим не только с использованием абстрактного поведенческого интерфейса, но и для коллекции any , а не только для списков. Сама структура данных сбора может решить, как выполнить траверс наиболее эффективно, например, он может решить выполнить его параллельно на нескольких ядрах процессора.
Существует лучший пример этого принципа в действии в оригинальной статье О понимании абстракции данных, Revisited Уильяма Р. Кука , в котором Кук объясняет фундаментальную разницу между объектно-ориентированной абстракцией данных и абстракцией данных на основе абстрактных типов данных. К сожалению, многие учебники относятся к объектам как к небольшому изменению ADT (например, «Objects = ADTs + Inheritance») или даже к одному и тому же, но они фактически принципиально разные. О критериях, которые будут использоваться в системах разложения в модули Дэвида Парнаса также по-прежнему хорошо читается, даже спустя 45 лет.
Один простой ответ на вопрос: «Почему это раскрывает реализацию?» заключается в том, что если у вас есть два приемника /сеттера, каждый из которых возвращает double и называется X и Y, вы подвергаете себя тому, что вы реализовали класс Point, используя Картезианские координаты , а не, скажем, Полярные координаты .
Кроме того, предоставление людям доступа к этим значениям X и Y может привести к их манипулированию ими при расчетах. Например; вместо использования
Point->distanceTo(otherPoint);
Они могут сделать что-то вроде:
distance = sqrt(
pow( PointA->getX() - PointB->getX(), 2 )
+ pow( PointA->getY() - PointB->getY(), 2 ) );
В дополнение к дублированию кода, которое неизбежно произойдет, это делает второе предположение. Эта формула работает только на плоской прямоугольной плоскости. Если вы фактически представляете сетку с шестиугольником, например, эта функция просто неверна.
Не раскрывая, как вы реализуете точку в поле, вы можете позже изменить между способами хранения этих координат и , чтобы вы не пытались пытаться быть умными с подробностями, которые они не должны касаться в первую очередь.
Я просто не согласен с этим предложением. Это зависит от семантики объекта, который вы представляете.
Ваш Point
должен быть объект значения (например, целое число, ...), что-то полностью определенное его состоянием. У вас может быть две разные переменные, содержащие номер 1, но номер 1 по своей сути уникален, как и код Point
(0,1). Этот класс должен быть неизменным, поэтому с getters для него X
И Y
отлично, а также геттеры для полярных координат R
и Thêta
. Внутреннее представление может быть также.
Класс, представляющий Cup
, может иметь свойство, представляющее его позицию. Поскольку я отлично умею перемещать Cup
произвольно, наличие геттера и сеттера в порядке. Может быть какое-то поведение, например, автоматическая настройка его Height
при его удалении (когда IsInMyHand
становится истинным).
С другой стороны, класс, представляющий Car
, не должен иметь установщика для его позиции (если вы не моделируете большой кран, например). Это положение должно зависеть от истории его GasPedal
и SteeringWheel
.
Мой ответ на ваш вопрос
За упрощением?
Существует идиома для скрытия частных данных. Для этого есть разные названия: Opaque Pointer, Pirom Idiom, Cheshire Cat и т. Д. См. https: //ru .wikipedia.org /wiki /Opaque_pointer для более подробной информации.
Я прочитал об этом в первый раз в крупномасштабном программном обеспечении на C ++ Джон Лакос. Он использовал термин «изоляция» для описания практики. Изоляция сильнее, чем инкапсуляция, насколько контроль над разработчиком должен изменить детали частных данных.
Re:
Почему автор говорит, что комбинирования геттеров, сеттеров и модификатора частного доступа недостаточно?
Недостаточно скрывать детали реализации публичных функций getter и setter. Недостаточно, если вы, разработчик Point
, хотите больше свободы в том, как представлены частные данные, и хотели бы, чтобы гибкость изменения представление, не затрагивающее пользователей Point
.
Re:
, то как это сделать (скрыть детали реализации объекта)?
Я не знаю языка вашего опубликованного фрагмента кода, но на C ++ это может быть что-то вроде:
Заголовочный файл, назовите его Point.h:
class Point {
public:
// Constuctor
Point();
// Destuctor
~Point();
// Getter functions.
double get_x() const;
double get_y() const;
// Setter functions.
void set_x(double x);
void set_y(double y);
// Need to add copy constructor and copy assignment operators too.
// Need to take care of the Rule of Three.
// Class to hold the data
class Data;
private:
// Pointer to the object holding private data
Data* dataPtr;
};
Файл реализации, назовите его Point.cpp:
#include "Point.h"
class Point::Data
{
public:
Data() : x(0), y(0) {}
double x;
double y;
};
Point::Point() : dataPtr(new Data()) {}
Point::~Point() { delete dataPtr; }
double Point::get_x() const { return dataPtr->x; }
double Point::get_y() const { return dataPtr->y; }
void Point::set_x(double x) { dataPtr->x = x; }
void Point::set_y(double y) { dataPtr->y = y; }
Это полностью скрывает, как хранятся личные данные Point
. Одна из интересных вещей, которая возникает в результате использования этой идиомы в C ++, состоит в том, что детали Data
могут быть изменены по желанию, не требуя какого-либо из файлы, которые зависят от Point.h, перекомпилируются. Это дает реальные преимущества в больших приложениях с сотнями .cpp-файлов.
Как уже упоминалось @Erik, интерфейс раскрывает тот факт, что он реализован с использованием декартовой системы координат.
Это только теоретический аргумент, поскольку иногда нецелесообразно not создавать точки просто (x, y) по таким причинам, как мгновенное знакомство и производительность. Но, расширяя теоретический аргумент, вы можете сказать, что это функция CoordinateSystem
, чтобы дать вам числовое описание местоположения определенной точки , В качестве примера можно использовать двойную отправку для преобразования точки из одной системы координат в другую , Например. в здесь .
interface Point {
Point convertTo(CoordinateSystem cs);
}
class CartesianPoint implements Point {
CartesianPoint(double x, double y) {
this.x = x;
this.y = y;
}
public Point convertTo(CoordinateSystem cs) {
return cs.convert(this);
}
double x;
double y;
}
class PolarPoint implements Point {
public PolarPoint(double angle, double radius) {
this.angle = angle;
this.radius = radius;
}
public Point convertTo(CoordinateSystem cs) {
return cs.convert(this);
}
double angle;
double radius;
}
interface CoordinateSystem {
Point convert(CartesianPoint p);
Point convert(PolarPoint p);
double distance(Point p1, Point p2);
}
class CartesianCoordinateSystem implements CoordinateSystem {
public Point convert(CartesianPoint p) {
return p;
}
public Point convert(PolarPoint p) {
double x = p.radius * Math.cos(p.angle);
double y = p.radius * Math.sin(p.angle);
return new CartesianPoint(x, y);
}
public double distance(Point p1, Point p2) {
CartesianPoint cp1 = (CartesianPoint)p1.convertTo(this);
CartesianPoint cp2 = (CartesianPoint)p2.convertTo(this);
double a = cp1.x - cp2.x;
double b = cp1.y - cp2.y;
return Math.sqrt((a * a) + (b * b));
}
}
class Program {
public static void main(String[] args) {
PolarPoint p1 = new PolarPoint(1.23, 5);
CartesianPoint p2 = new CartesianPoint(4, 6);
CartesianCoordinateSystem cs = new CartesianCoordinateSystem();
CartesianPoint cp1 = (CartesianPoint)cs.convert(p1);
System.out.println("P1=(" + cp1.x + "," + cp1.y + ")");
System.out.println("P2=(" + p2.x + "," + p2.y + ")");
System.out.println("DISTANCE=" + cs.distance(p1, p2));
}
}
Это основная точка (каламбур).
Создание атрибутов класса private делает скрытие реализации и предоставление публичных функций, таких как double getX()
не раскрывать детали реализации как таковые.
Проблема в том, что все ожидают, что эта функция будет реализована следующим образом:
double getX() { return this.x; }
, а не, например, следующим образом:
double getX() { return this.radius * Math.cos(this.angle); }
Название функции поддерживает определенную реализацию.
По этой причине я стараюсь избегать имен функций, начиная с get
или set
как можно больше.
Если вам нужно извлечь декартову X-координату точки, вы можете назвать ее следующим:
double extractCartesianX() { ... }
Это имя не означает, что X фактически является атрибутом класса. Это может быть, но это также может быть рассчитанное значение.
Примечание. Я попытался дать краткий ответ, который напрямую касается вашего вопроса. Для получения дополнительной информации и справочной информации вы должны проверить отличный ответ на @CandiedOrange.
Автор какой-то книги сказал: «Это раскрывает реализацию. В самом деле, она будет раскрывать реализацию, даже если переменные были частными, и мы использовали одноразовые геттеры и сеттеры». Автор абсолютно прав. Но в этом случае ответ: «И что?»
Класс Point представляет собой точку, состоящую из двух координат двойной точности. Пользователь класса Point может считывать и назначать каждую координату отдельно. Это полностью соответствующий интерфейс - класс, представляющий математическую точку. Конечно, реализация раскрывается, но в этом случае это абсолютно нормально.
Нарушение реализации плохо, если изменение реализации, сохраняя целостность интерфейса, вызовет проблемы, поскольку реализация была обнаружена. В этом случае интерфейс, а не реализация, представляет собой две доступные переменные экземпляра двойной точности. Поэтому я не могу понять, как кто-то может изменить реализацию без изменения интерфейса.
Я бы сказал, что автор книги сделал ужасный пример (не то, что класс Point ужасен, это абсолютно нормально, он использует его в качестве примера для зла, чтобы разоблачить реализацию, которая ужасна). Он или она должны были использовать пример, когда подверженность реализации на самом деле вызывает проблемы.