Войти
ФлеймФорумПрограммирование

Почему в ООП языках есть объявление подклассов, но нет объявления надклассов?

Страницы: 1 2 3 4 5 Следующая »
#0
4:31, 31 дек. 2015
Необходимая преамбула.
ООП - говно. Это, увы, так. Но, как и с любым говном, нам приходится с ним жить и даже производить его самим. С этим фактом нельзя не считаться. Поэтому тема про ООП.

Как мы знаем, в ООП языках есть декларация класса-наследника какого-нибудь одного или нескольких классов-предков, таких, что класс-наследник включает содержимое предков, и объект класса-наследника является одновременно объектом классов-предков:
class A1 { a1_1; a1_2; }
class A2 { a2_1; }
class B extends A1, A2 { b_1; }

A1 a1 = new A1;
a1.a1_1; a1.a1_2; // A1 содержит содержимое A1
A2 a2 = new A2;
a2.a2_1; // A2 содержит содержимое A2

B b = new B;
b.a1_1; b.a1_2; b.a2_1; b.b_1; // B содержит содержимое A1, A2 и B

// класс B есть класс A1 и есть класс A2, но не наоборот
a1 = b; // ok
a2 = b; // ok
b = a1; // NOT OK
b = a2; // NOT OK
Но почему абсолютно нигде нет обратной операции - декларации класса-предка какого-нибудь одного или более классов? Чтобы все потомки при этом включали содержимое предка и являлись объектами класса-предка:
class B1 { b1_1; b1_2; }
class B2 { b2_1; }
class A supers B1, B2 { a_1; }

B1 b1 = new B1;
b1.b1_1; b1_2; a_1; // B1 содержит содержимое B1 и A
B2 b2 = new B2;
b2.b2_1; a_1; // B2 содержит содержимое B2 и A

A a = new A;
a.a_1; // A содержит содержимое A

// классы B1 и B2 есть класс A, но не наоборот
a = b1; // ok
a = b2; // ok
b1 = a; // NOT OK
b2 = a; // NOT OK
Отсутствие такой возможности - признак неполноты и, как следствие, глубокой логической ущербности современной ООП парадигмы (что лишний раз констатировано в преамбуле).

Могут возникнуть псевдовозражения, но они не выдерживают элементарной критики и по существу являются просто отмазками:

Q. А на хрена это нужно?
A. На очень даже много какого хрена это может быть нужно. Только навскидку 2 сценария:
1) когда имеем некий тип данных, который может принимать значение одного из типов, ранее известных, но, в силу дизайна, никак друг с другом не связанных. А-ля union/bu$t::any, только круче:

class/interface Number {...}
class Infinity { bool plus; }
class NaN {}

class RealNumber supers Number, Infinity, NaN
{
 // но, в отличие от банального юниона, это самостоятельный класс, и через него можно имплантировать методы в потомков:
 bool isNaN () { return (this is NaN); }
}

int i = 1;
Rational r = 2;
GMPInt api = 4;
RealNumber rn1 = i, rn2 = NaN ();

// i, r и api обретают метод isNaN в силу обретения предка RealNumber
if (!i.isNaN ()) printf ("i is number\n");
if (!r.isNaN ()) printf ("r is number\n");
if (!api.isNaN ()) printf ("api is number\n");
// ес-но, он есть и у rn
if (!rn.isNaN ()) printf ("rn is number\n");

// и ещё можно в качестве RealNumber сувать значение любого из этих классов
RealNumber calculate (RealNumber r)
{
 return rand ()%2? NaN () : int (0);
}
rn2 = calculate (i);
rn2 = calculate (r);
rn2 = calculate (rn1);

// кроме того, от этого класса мы можем наследовать другие!
class OurBCDNumber extends RealNumber { ... }
rn1 = calculate (OurBCDNumber (...));
Или, например, можно так:
class Null {};
template [class T]
class Nullable supers T, Null
{
 function isNull
}

Nullable[File] openFile (String fileName); // возвращает File или Null (если по сигнатуре ещё не ясно)
// таким образом упраздняется нужда в такой странной встроенной в язык конструкции, как null,
// и исчезают связанные с ней проблемы

2) когда нужно не расширять выставляемый в пространство интерфейс, а наоборот, уменьшить:

template [class T]
class GovnoCollection
{
 T read ();
 void write (T x);
}
// а нам нужно как-нибудь явно указать, что хотим коллекцию, из которой можно только читать, что делать?

template [class T]
class ReadOnlyCollection supers GovnoCollection[T]
{
 abstract T read (); // как бы сигнализируем, что в классах-потомках будет обязательно определён read
}

void giveMeReadOnlyCollection (ReadOnlyCollection[GameObject] roc);
GovnoCollection[GameObject] gc = ...;
giveMeReadOnlyCollection (gc);

// таким образом упраздняется нужда в такой странной встроенной в язык конструкции, как const/mutable,
// и исчезают связанные с ней проблемы
То есть, введением такой фичи можно одним махом упростить язык на целых два весьма застарелых и граблеопасных костыля.

Q. А как это реализовать? В случае потомка мы, например, дописываем расширяющие классы в конец структуры, а тут как? Встаёт, например, проблема классов, которые декларированы до того, как провозглашён их общий предок - куда там вставлять новые члены?
A. Реализуется не так уж сложно. Например, так (для языка парадигмы "переменная=ссылка, рулит ГЦ"):

class A supers B1, B2, B3 {...members_of_A...}

// на самом деле
class A::__container
{
 members_of_A;
 Object actualData;
}

static WeakBiMap<Object::IdentityKey,A::__container> A::containers;
// специальный слабый мап, элементы из которого удаляются только тогда, когда не осталось внешних ссылок ни на ключ, ни на значение
A::__container A::__get_A_for_object (Object object)
{
 if (!containers.exist (Object::getIdentityKey (object)))
  containers.put (Object::getIdentityKey (object),new __container { actualData = object; });
 else return containers[Object::getIdentityKey (object)];
}
Если переменная или значение B1, B2, B3 уже существуют - пусть себе существуют, как и раньше, и ничего не знают про A. Инициализация контейнера под A и членов A случается в тот момент, когда производится каст из B1/B2/B3 в A (в т. ч. при использовании на них методов A).
B1 b1; // ok
// никакого A пока ещё нет

A a = b1;
// на самом деле:
A::__container a = A::__get_A_for_object (b1);

void takeAnA (A input);
takeAnA (b1);
// на самом деле:
takeAnA (A::__get_A_for_object (b1));

takeAnA (a);
// а в этом случае всё буквально
Для парадигмы Ц++ придётся извратиться посложнее, но было бы желание. И, разумеется, возможны всяческие оптимизации (никто не говорит, что структура класса обязана быть железной на все случаи жизни).

Немного подрывает шаблон нарушение привычной концепции "к постройке производного типа уже построен базовый". Но является ли эта концепция принципиально необходимой? Если подумать, с удивлением окажется, что нет. Если производный тип внутри себя никак не завязан на базовый, а снаружи про его базу не знают, то и зачем она? Соответственно, можно её инициализировать только по необходимости, или вообще исключать (тут ещё больший простор для оптимизации).

Наконец, ещё одно, вполне естественное замечание: класс не может быть одновременно supers и extends. Либо одно, либо другое. Ибо иерархия классов всё же не должна быть циклической.

Q. Получаются логические дыры в семантике. Вот, например, объявлен тип:

class Point2D
{
 float x,y;
 // а в нём конструкторы
 Point2D (Number x,Number y); 
 Point2D (Point2D src);
}
Внезапно мы объявляем ему надкласс тоже с непустым конструктором:
class Point1D supers Point2D
{
 abstract float x;
 float module;
 // и в нём тоже конструктор
 Point1D (Trollface problems);
}
Получается, класс Point2D должен вызвать конструктор Point1D, но как он это сделает, если он на момент своей имплементации ничего про Point1D не знает?
A. В предыдущем ответе мы упразднили необходимость вызова конструкторов от базы к потомкам, так что данный вопрос решается просто: если Point2D для своей нормальной работы ничего не нужно знать про Point1D, то он ничего и не должен.
Более того, в нашем случае расклад инвертируется: это Point1D нужно знать про Point2D (по построению). Поэтому в языке появляется фича, взрывающая закоснелый моск - возможность (и порой даже требование) вызвать конструктор производного класса из конструктора базового:
class Point1D supers Point2D
{
 abstract float x;
 float module;
 Point1D (Point2D this) // если в Point2D нет дефолтного конструктора, то без конструктора Point1D (Point2D this) - ошибка компиляции
 {
  module = sqrt (x*x+y*y);
 }

 // а вот наш конструктор из каверзного вопроса. В Point2D он не задействован, однако будет задействован в классах, которые будут наследоваться от Point1D традиционным образом (через extends Point1D).
 Point1D (Trollface problems);
}

А конвенция об инициализации "дописных" частей только по необходимости позволяет разруливать и вот такие рекурсии:

class IntNumber supers char, int, long
{
 IntNumber (char this) { bits = 8; }
 IntNumber (int this) { bits = 32; }
 IntNumber (long this) { bits = 64; }
 int bits; // но постойте, это ведь тоже IntNumber!
}

// ну и чо?
int x = 100500;
printf ("%d\n",x.bits); // IntNumber для x создаётся только здесь
printf ("%d\n",x.bits.bits); // IntNumber для x.bits создаётся только здесь

Q. А классы "старого стиля", наследники с указанием расширения? У них инициализация базы тоже станет ленивой?
A. Нет, не станет. Точнее, старый стиль вписывается в логику "по необходимости" и так. То, что в "старом стиле" потомкам требуются (некоторые определённые) базы, следует из декларации - мы явно прописываем нужное в списке extends. И логика их работы и инициализации - как раньше. А в "новом стиле", наоборот, потомкам базы не нужны, но базам нужны (некоторые определённые) потомки - и это тоже следует из декларации, мы явно прописываем нужное в списке supers. Кругом логика и строгость.

Q. А что должно получиться в этой ситуации:

class B1 extends A {}
class B2 extends A {}

class A1 supers B1, B2 {}
?
A. Ну так и будет - в смысле отношений между классами аналогично
class A; class A1;
class B1 extends A,A1 {}
class B2 extends A,A1 {}
(но не в смысле "кто кому нужен" - о том, кто кому нужен, см. по правилам выше.)
С другой стороны, что мы пытались сказать вышеприведённой заявкой? Создать надкласс для B1 и B2, который был бы в то же время наследником A? Так это прямо запрещено - класс не может объявляться одновременно и надклассом, и подклассом уже существующих классов. Впрочем, учитывая осмысленность паттерна, можно добавить в язык сахера:
class A1 supers B1, B2 abstract A {}
// сам класс A1 _не_ является наследником A, но на всех его потомков (в т. ч. B1 и B2) налагается требование быть также явными наследниками A
// в силу этого допускается неявный каст из A1 в A и использование из A членов A1, но одновременно запрещается независимая инстанцинация самого A1

Q. А вот в языках парадигмы "переменная=ссылка, рулит ГЦ" обычно все классы являются неявными потомками некого Object. Но кем тогда будет надкласс (особенно если надкласс над Object)?
A. Надклассы не являются потомками Object (что, в принципе, и логично), но все их наследники, за исключением других надклассов, обязаны быть потомками Object. То есть, выражаясь на языке предыдущего Q:

class Superclass supers A,B {}
// аналогично
class Superclass supers A,B abstract Object {}

Домашнее задание: поразмыслить, реалзуемо ли вышеописанное расширение парадигмы средствами существующих языков говноООП.


#1
5:50, 31 дек. 2015

Ктож это все читать будет?

#2
5:56, 31 дек. 2015

Домашнее задание для Sbtrn. Devil на новый год:
Оформить эту красивую русскую стену текста на компьютерном английском, в виде предложения для комитета стандартизации С++
Инструкция по оформлению здесь:
https://isocpp.org/std/submit-a-proposal

А также поразмыслить над краевыми условиями.
Что случится, если объявления классов противоречат друг другу?

class B extends A {};
class A supers C {};
class C supers B {};

Что случится, если объявления классов вроде бы не противоречили друг другу и всё скомпилилось, и здесь добавился новый модуль в котором внезапно всё стало противоречить?

// было
class A {};
// и мы его уже создали, использовали, и всё скомпилилось.
// но вот внезапно позже в процессе компиляции поинклюдился заголовок.h и стало
class B supers A { int fgsfds_; };
// старый код получается невалиден? и класс А уже имеет другой размер и вообще сам на себя не похож.

#3
8:56, 31 дек. 2015

Пусть PDF выпускает :)))

PS: Тема выглядит по страшнее квантовой физики.

#4
10:28, 31 дек. 2015

>class B1 { b1_1; b1_2; }
>class B2 { b2_1; }
>class A supers B1, B2 { a_1; }

template <typename T>
class B1 : T { b1_1; b1_2; };

template <typename T>
class B2 : T { b2_1; };

class A { a_1; };

typedef B1<A> B1A;
typedef B2<A> B2A;

#5
11:00, 31 дек. 2015

Sbtrn. Devil
На днях почитай про экстеншен методы; трейты; миксины; аспекты и аспектно ориентированное программирование (аоп).

#6
11:14, 31 дек. 2015

amd.fx6100
> Ктож это все читать будет?
Ну не поленился же этот кто-то написать ответ в целых 5 слов - глядишь, и почитать найдёт в себе силы.

kvakvs
> Оформить эту красивую русскую стену текста на компьютерном английском, в виде
> предложения для комитета стандартизации С++
> Инструкция по оформлению здесь:
В комитете Ц++ сидят враги Ц++, задавшиеся целью планомерно погубить язык. Туда писать уже бесперспективно - логичные и здравые предложения там не принимаются.

> А также поразмыслить над краевыми условиями.
> Что случится, если объявления классов противоречат друг другу?
> class B extends A {};
> class A supers C {};
Невозможно. Субклассить или надклассить только завершённые классы, а A на момент декларации B и C на момент декларации A ещё не завершены.
(Ц++, в котором принят именно такой порядок, в этом месте ехидно хихикает над конкурентами, которые исповедуют подход "неупорядоченная свалка классов", а конкуренты, почёсывая репу, размышляют в сторону введения ошибки "у вас получилась циклическая иерархия".)

> // было
> class A {};
> // и мы его уже создали, использовали, и всё скомпилилось.
> // но вот внезапно позже в процессе компиляции поинклюдился заголовок.h и
> стало
> class B supers A { int fgsfds_; };
> // старый код получается невалиден? и класс А уже имеет другой размер и вообще
> сам на себя не похож.

Sbtrn. Devil
> Если переменная или значение B1, B2, B3 уже существуют - пусть себе существуют,
> как и раньше, и ничего не знают про A. Инициализация контейнера под A и членов
> A случается в тот момент, когда производится каст из B1/B2/B3 в A (в т. ч. при
> использовании на них методов A).
> B1 b1; // ok
> // никакого A пока ещё нет
>
> A a = b1;
> // на самом деле:
> A::__container a = A::__get_A_for_object (b1);
>
> void takeAnA (A input);
> takeAnA (b1);
> // на самом деле:
> takeAnA (A::__get_A_for_object (b1));
>
> takeAnA (a);
> // а в этом случае всё буквально

TarasB
> template <typename T>
> class B1 : T { b1_1; b1_2; };
>
> template <typename T>
> class B2 : T { b2_1; };
>
> class A { a_1; };
>
> typedef B1<A> B1A;
> typedef B2<A> B2A;
Вариант, конечно, но всё ж таки несколько ущербный. Мы тут как бы явно ожидаем, что B1 и B2 будут суперкласситься, а A, например, уже нет. Да и A не знает про B1 и B2, что подрывает его смысл как предка известных классов.

#7
12:17, 31 дек. 2015

Sbtrn. Devil
> Мы тут как бы явно ожидаем, что B1 и B2 будут суперкласситься,
Да, и этом мой вариант лучше твоего.

#8
12:30, 31 дек. 2015

В расте есть трейты, они могут все это и даже больше. Впрочем, не все считают раст ООП языком.

#9
12:57, 31 дек. 2015

всё больше не буду делать надклассы

#10
13:26, 31 дек. 2015

Sbtrn. Devil
Возьми, да запили свой концептуальный язык, в чём проблема-то?

#11
13:30, 31 дек. 2015

В рамках текущего синтаксиса вроде же можно все тоже самое сделать. Создается класс-родитель и указывается во всех наследниках. А так иерархия становится практически  нечитаемой, особенно в крупных проектах.

#12
16:33, 31 дек. 2015

$tatic
> Возьми, да запили свой концептуальный язык, в чём проблема-то?
У меня в планах другой концептуальный язык, очередь занята.

TarasB
> Да, и этом мой вариант лучше твоего.
Он именно этим хуже, потому что идея как раз в том, чтобы классу не обязательно было знать, какие у него предки, помимо тех, которые ему непосредственно нужны. Тут как в теории множеств: мы рассматриваем множество А, которое, в принципе, может быть как надмножеством для каких-то там Б1, Б2, ..., так и подмножеством для каких-то там Ц1, Ц2, ... но для имеющейся задачи нам достаточно того, что это просто множество А, поэтому упоминать ни Бn, ни Цn в формулировке условий нет необходимости.

GeniusIsme
> В расте есть трейты, они могут все это и даже больше. Впрочем, не все считают
> раст ООП языком.
Это интересное примечание. Впрочем, руст - действительно странный язык, идеи которого то ли опережают время, то ли развиваются в перпендикулярную временную ось.

Serge
> Создается класс-родитель и указывается во всех наследниках.
Это не то. Соль-то в том, чтобы как раз не нужно было указывать.

> А так иерархия становится практически нечитаемой, особенно в крупных проектах.
Иерархия в крупных проектах и так практически нечитаемая. Счастье не в читаемости иерархии, а в возможности статического контроля и формализуемости отношений.
Собственно, что меня окончательно навело на мысль обо всём этом. В жабе есть "просто коллекции" (для чтения-записи), но нет "коллекций только для чтения" (есть такие, в которых можно запретить вызовы пишущих методов, но это делается в рантайме и средствами кода не выражается). И довелось в связи с этим наступить на грабли в индусском коде:

class Objekt
{
 Objekt (Set<T> items);
 Set<T> getItems ();
}
// афтор класса как бы намекает, что желает получить для конструкции готовый набор итемов

// но в другом месте кода...
Objekt obj = new Objekt (new HashSet<T> ());
obj.getItems ().add (getItemsFromSomeSource (...));
// при определённой реализации Objekt это даже давало правильный результат
Для борьбы с граблями объект был переделан вот так:
class Objekt
{
 Objekt (Set<T> items);
 ImmutableSet<T> getItems (); // из гуавы, немутабельная копия сета
}
Грабли это, конечно, отодвигает в сторону, но ущербность концепции режет глаз. ImmutableSet сделан как наследник Set (потому как более никакого другого отношения с Set-ом язык установить в принципе не позволяет), поэтому тащит в себе все методы Set-а, включая писательные - они помечены депрекейтами, которые смотрят жалобными взглядами костыля, знающего, что он костыль, и просящего не судить строго. И грабли хоть и отодвигаются, но окончательно не устраняются. Можно подать этот ваш ImmutableSet в функцию, которая берёт просто Set (подразумевая, что будет в него писать), и никак компилятору (и кодопишущему индусу) не намекнуть, что это неправильно.
Вопиюще напрашивается наличие класса ReadOnlySet, который бы предшествовал Set-у - его-то и следовало бы и давать параметром в конструктор, и возвращать в getItems... но увы, в жабной библиотеке такого не предусмотрели. А иного решения язык не предоставляет.

#13
18:59, 31 дек. 2015

Sbtrn. Devil
То есть это любому классу можно присобачить предка даже если он этого не хотел да это же мина. И как ты пользоваться то собрался этим.
Идея имла бы смысл если бы новая база реально содержала лишь некоторые поля и методы наследника и не имела ничего своего.

#14
20:25, 31 дек. 2015

TarasB
> И как ты пользоваться то собрался этим.
Как-как. Вставить костыль да и все. Эта херня позволит вставлять костыли на любом уровне и сделает код совершенно нечитаемым.

Sbtrn. Devil
У вас в семье говядину не едят, да?

Страницы: 1 2 3 4 5 Следующая »
ФлеймФорумПрограммирование