Динамическая классификация объектов
От: borisman2 Киргизия  
Дата: 01.10.04 09:24
Оценка: 71 (6)
+ Письмо в RSDN


— <<Приветсвие>>
Здравствуйте товарищи программисты!

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

Строго говоря, этот пост не является каким-то конкретным вопросом. Просто я излагаю свою небольшую идею и хочу знать, как эту идею примет уважаемое сообщество RSDN. Единственная просьба — не бейте камнями, а точнее вопросами — "Да зачем все это надо ??? У меня вот нет таких проблем... потому что я пишу вот так мол и так...", поймите, я не могу отмахнуться от задачи, просто отказавшись ее решать.
— <<Введение>>
Меня зовут Борис, я работаю в Программе Микрофинансирования Европейского Банка Реконструкции и Развития. Одна из задач, которые передо мной стоят — задача отслеживания заявок, кредитов, заемщиков и т.д. в банках-партнерах, с которыми работает наша Программа. Не вдаваясь а дальнейшие детали, перейду непосредственно к вопросу.

+ <<Задача: модель данных>>
В настоящий момент я разрабатываю модель данных для довольно сложной комплексной проблемы учета. Модель включает в себя массу информации из реального мира, которую не так то просто промоделировать. Конечным результатом должен стать модуль, написанный на языке Python, т.е. грубо говоря, некая иерархия классов, отражающая предметную область.

Я буду приводить примеры на С++ чтобы было более понятно аудитории, потому как по данным опроса большинство программистов RSDN используют C++. Пожалуйста, отнеситесь со снисхождением к коду, который я буду писать на С++, потому что у меня нет возможности (да и желания) его хотя-бы раз скомпилировать, а потому возможны разные синтаксические ошибки.
+ <<Основной затык>>
Разрабатывая проблему, я столкнулся со следующим весьма печальным (и, в общем-то, известным) затыком: статическая объектная модель данных (т.е. иерархия классов) не может достаточно полно и точно описать предметную область.

Сразу оговорюсь, что при разработке модели Я НЕ ПЫТАЛСЯ объять необъятное, т.е. разработать модель для всех возможных случаев. Это попросту невозможно.
— <<Пример>>
В предметной области имеются люди как таковые, то есть некоторые объекты, имеющие фамилию имя и отчество (строго говоря, для какой-то другой предметной области важнее будет, что люди имеют, скажем, рост и вес, но в данном случае я разрабатываю совершенно конкретную областьи потому важно именно ФИО), на C++ это может быть описано примерно так:


class Person {
    public string first_name;
    public string last_name;
    public string third_name;
}


Люди могут при этом быть пользователями системы:

class User: public Person {
    public string login;
    public string password;
}

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

class Applicant: public Person {
    public Application application;
    public date applicationDate;
}


Может быть такая ситуация, что человек является подателем заявления (Applicant) но не является пользователем системы (User). А может быть и наоборот — человек может являться пользователем системы и не являться подателем заявления.
— <<Почему так происходит>>
Проблема в том, что статическое описание классов (диаграмма классов) не может описать динамический характер предметной области.
— <<Обычное решение>>
Обычное решение (к сожалению) состоит в том, чтобы отказаться от классического ОО моделирования предметной области, исскуственно упрощая модель данных, например:

class User {
    public string first_name;
    public string second_name;
    public string third_name;
    public string login;
    public string password;
}

class Appplicant {
    public string first_name;
    public string second_name;
    public string third_name;
    public Application app;
}

Преимущества и недостатки такого подхода говорят сами за себя:
\- Это проще реализовать
\- К сожалению, теперь уже ПРИЛОЖЕНИЕ должно знать о связях между объектами, т.е. приложение должно знать, скажем, что пользователь и заявитель с одинаковыми ФИО — это одно и то же лицо.


— <<Возможное статическое решение>>
Ранее я уже сталкивался с подобной проблемой в более мелких масштабах и решал я ее так:

class Person {
    ...
}

class User {
    public Person person;
}

class Applicant {
    public Applicant applicant;
}


Далее все понятно. Но есть несколько затыков:
1) Можно определить, что пользователь является человеком, но не наоборот (нельзя определить что данный конкретный человек является пользователем)
2) Четкую иерархию классов мы подменили исскуственной иерархией агрегации. Это, в общем, терпимо, однако заставляет нас писать дополнительный проблемно-ориентированный код, который реализует иерархию. Это "грязный хак".


До сих пор я считал, что это вообще единственное разумное решение.
+ <<Шаблонный подход, приближающий нас к идеалу>>
— <<Множественное наследование>>
Выразить факт того, что человек является пользователем или заявителем можно, используя множественное наследование
class PersonMixin {
    public string first_name;
    public string second_name;
    public string third_name;
}

class UserMixin {
    public string login;
    public string password;
}

class Applicant {
    public Application application;
}

теперь конкретный объект можно создать так:
class ConcreteUser: public PersonMixin, public UserMixin {
}
ConcreteUser user = ConcreteUser()

Множественное наследование не такая уж страшная вещь, как ее малюют. До тех пор, конечно, пока Вы не заморачиваетесь с виртуальными классами, т.е. пока классы, от которых Вы наследуете, не слишком сложны.
— <<Шаблоны>>
Шаблоны позволяют упростить использование множественного наследования
template <typename T, typename B> class Dual: public <T>, public <B> {
}

user = Dual<PersonMixin, UserMixin>()


примерно так или что-то в этом духе.
— <<Затык>>
К сожалению, и этот подход ведет в тупик, потому что невозможно изменить класс объекта в рантайме, т.е. если, например, был у нас Петр Петрович Иванов, и стал он вдруг пользователем системы, это мы уже не сможем выразить в коде. Вот если ЗАРАНЕЕ известно, что Петр Петрович является пользователем системы — тогда все Ок.
— <<Динамическое решение>>
К большому моему счастью (и, конечно, к несчастью, потому что в мире вообще сплошные палки с двумя концами) я пишу задачу не на С++, а на Питоне.
Питон является динамическим языком с очень мощными возможностями интроспекции. В частности, он позволяет изменять класс переменной ВО ВРЕМЯ ИСПОЛНЕНИЯ ПРОГРАММЫ!

Идея, кажется, не новая, потому что смутно помню, что я это уже где-то встречал. Но простота, с какой ее можно реализовать на Питоне, меня поразила...

Поэтому. Можно писать код в таком виде.

obj = object() # какой-то абстрактный объект

promote(obj, User) # объект стал пользователем
obj.login="login" 
obj.password="password"

promote(obj, Person) # объект кроме того, что был пользователем, стал еще и человеком
obj.firstName = "Пасько"
obj.firstName = "Борис"
obj.firstName = "Сергеевич"

promote(obj, Person) # объект кроме того, что был пользователем и человеком, стал еще и заявителем
obj.application = Application(...)

Определения классов притимивны, как и в шаблонном примере:
class User:
    login = None
    password = None

class Person:
    firstName = None
    lastName = None
    thirdName = None
    
class Applicant:
    application = None

Реализация функции promote очень проста, но для ее понимания Вам понадобится разобраться в том, как организованы классы в Питоне. Можете этот пункт просто пропустить
def promote(obj, klass):
    bases = obj.__class__.__bases__
    bases = list(bases) # потому что obj.__class__.__bases__ - это не список, а tuple
    bases.append(klass) # !!!
    obj.__class__.__bases__=tuple(bases)





— <<Что это дает???>>
А дает такой подход несколько приятных возможностей.
1) Мы не ломаем ООП подход к нашей модели, просто рассматриваем возможность того, что классы могут изменяться в рантайме, т.е. расширяем ОО моделирование и на рантайм.
2) Мы можем использовать базовые возможности языка для работы с такими объектами. Например, при агрегационно подходе для того, чтобы изменить фамилию пользователя мы написали бы что-то вроде:
    
user.person.firstName = "Иванов"

в данном случае мы просто пишем
user.firstName = "Иванов"

Мы можем использовать явные проверки для определения класса объекта:

isinstance(obj, User)
isinstance(obj, Applicant)
...
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.