Проблема неустойчивости базовых классов и контроль версии
Проблема несовместимости компонентов хорошо известна всем, кому доводилось программировать для Windows. Обычно она выступает в форме так называемого кошмара DLL (DLL Hell) — программа использует определенную версию DLL, a потом установка новой версии компонента нарушает работу программы. Почему? Причины могут быть разными, от очевидных (случайное исключение функции, использовавшейся в программе) до весьма нетривиальных (например, изменение типа возвращаемого значения у функции). В любом случае все сводится к вариациям на одну тему — при изменении открытого интерфейса кода, от которого зависит ваша программа, программа не может использовать новую версию вместо старой, а старая версия уже стерта. В большинстве объектно-ориентированных языков наследование сопряжено с потенциальной угрозой работоспособности вашей программы из-за несовместимости компонентов. Программисту остается лишь надеяться на то, что открытые и защищенные члены классов-предшественников в 1 иерархии наследования не будут изменяться, таким образом, что это нарушит ра-
ботоспособность их программ. Эта ситуация называется проблемой неустойчивости базовых классов. Наследование часто превращает наши программы в некое подобие карточного домика — попробуйте вытащить нижнюю карту, и все сооружение развалится.
Проблему неустойчивости базовых классов желательно рассмотреть на конкретном примере. Разместите приведенное ниже определение класса Payabl eEntity в отдель-ной^библиотеке и откомпилируйте его в сборку с именем PayableEntity Example командой Build (чтобы задать имя сборки, щелкните правой кнопкой мыши на имени проекта в окне решения, выберите в контекстном меню команду Properties и введите нужные значения в диалоговом окне). Если вы не используете архив с примерами, прилагаемый к книге, запомните, в каком каталоге был построен проект:
Public Mustlnherit Class PayableEntity
Private m_Name As String
Public Sub New(ByVal theName As String)
m_Name =theName
End Sub
Public Readonly Property TheName()As String Get
Return m_Name
End Get
End Property
Public MustOverride
Property TaxID()As
String End Class
После построения DLL закройте решение.
Допустим, вы решили включить в класс Employee новый способ получения адреса, зависящий от базового класса PayableEntity; при этом следует помнить, что класс будет использоваться только в откомпилированной форме. Для этого необходимо включить ссылку на сборку, содержащую этот проект (находится в подкаталоге \bin того каталога, в котором была построена DLL PayableEntityExample). Примерный код класса Empl oyee приведен ниже. Обратите внимание на строку, выделенную жирным шрифтом, в которой класс объявляется производным от абстрактного класса, определенного в сборке
PayableEntityExample.
Public Class Employee
' Пространство имен называется PayableEntityExample.
' поэтому полное имя класса записывается в виде
PayableEntityExample.PayableEntity! Inherits
PayableEntityExample.Employee
Private m_Name As String
Private m_Salary As Decimal
Private m_Address As String
Private m_TaxID As String
Private Const LIMIT As Decimal = 0.1D
Public Sub New(ByVal theName As String,
ByVal curSalary As Decimal,
ByVal TaxID As String)
MyBase.New(theName)
m_Name = theName
m_Salary = curSalary
m_TaxID = TaxID
End Sub
Public Property Address()As String
Get
Return m_Address
End Get
Set(ByVal Value As String)
m_Address = Value
End Set
End Property
Public Readonly Property Salary()As Decimal Get
Return m_Salary «
End Get
End Property
Public Overrides Property TaxIDO As String Get
Return m_TaxID
End Get
SetCByVal Value As String)
If Value.Length <> 11 Then
' См. главу 7 Else
m_TaxID = Value
End If
End Set
End Property
End Class
Процедура Sub Main выглядит так:
Sub Main()
Dim torn As New EmployeeC'Tom". 50000)
tom.Address ="901 Grayson"
Console.WriteCtom.TheName & "lives at " & tom.Address)
Console. ReadLine()
End Sub
Результат показан на рис. 5.7.
Программа работает именно так, как предполагалось.
Рис. 5.7. Демонстрация неустойчивости базовых классов (контроль версии отсутствует)
Программа компилируется в исполняемый файл Versiomngl.exe, все идет прекрасно.
Теперь предположим, что класс PayableEntity был разработан независимой фирмой. Гениальные разработчики класса PayableEntity не желают почивать на лаврах! Заботясь о благе пользователей, они включают в свой класс объект с адресом и рассылают новый вариант DLL. Исходный текст они держат в секрете, но мы его приводим ниже. Изменения в конструкторе выделены жирным шрифтом:
Imports Microsoft.Vi sualBasic.Control Chars
Public Class PayableEntity
Private m_Name As String
Private m_Address As Address
Public Sub New(ByVal theName As String,ByVal theAddress As Address)
m_Name = theName
m_Address = theAddress
End Sub
Public Readonly Property TheName()As String Get
Return m_Name End Get
End Property
Public Readonly Property TheAddress() Get
Return
m_Address.DisplayAddress
End Get
End Property
End Class
Public Class Address
Private m_Address As String
Private m_City As String
Private m_State As String
Private m_Zip As String
Public Sub New(ByVal theAddress As String.ByVal theCity As String.
ByVal theState As String.ByVal theZip As String)
m_Address = theAddress
m_City = theCity
m_State = theState
m_Zip = theZip
End Sub
Public Function DisplayAddress() As String
Return m_Address & CrLf & m_City & "." & m_State _
&crLF & m_Zip
End Function
End Class
Перед вами пример редкостной халтуры. В процессе «усовершенствования» авторы умудрились потерять исходный конструктор класса PayableEntity! Конечно, такого быть не должно, но раньше подобные катастрофы все же случались. Старая DLL устанавливалась на жесткий диск пользователя (обычно в каталог Windows\System). Затем выходила новая версия, устанавливалась поверх старой, и вполне благополучная программа Versioningl переставала работать (а как ей работать, если изменился конструктор базового класса?).
Конечно, проектировщики базовых классов так поступать не должны, однако на практике бывало всякое. Но попробуйте воспроизвести этот пример в .NET, и произойдет настоящее чудо: ваша старая программа будет нормально работать, потому что она использует исходную версию Payabl eEnti ty из библиотеки, хранящейся в каталоге \bin решения Versioningl.
Решение проблемы несовместимости версий в .NET Framework в конечном счете основа-но на том, что ваш класс знает версию DLL, необходимую для его работы, и отказывается работать при отсутствии нужной версии. Успешная работа этого механизма зависит от особых свойств сборок (см. главу 13). Тем не менее.в описанной нами ситуации защита .NET Framework преодолевается простым копированием новой DLL на место старой.
Схема контроля версии в .NET позволяет разработчикам компонентов дополнять свои базовые классы новыми членами (хотя на практике делать этого не рекомендуется). Такая возможность сохраняется даже в том случае, если имена новых членов совпадают с именами членов, включенных вами в производный класс. Старый исполняемый файл, созданный на базе производного класса, продолжает работать, поскольку он не использует новую DLL.
Впрочем, это не совсем верно: он действительно продолжает работать — до тех пор, пока вы не откроете исходный текст приложения Versioningl в VS .NET, создадите ссылку на DLL PayableEntityExample и попробуете построить приложение Versioningl заново. Компилятор выдаст сообщение об ошибке:
C:\book to comp\chapter 5\Versioningl\Versioningl\Modu1el.vb(21):
No argument specified or non-optional parameter 'theAddress' of
'Public Sub New(theName As String,theAddress
As PayableEntityExample.Address)'.
Итак, как только вы загрузите старый исходный текст производного класса и создадите ссылку на новую DLL, вам не удастся откомпилировать программу до исправления той несовместимости, на которую вас обрекли разработчики базового класса.
Прежде чем завершить этот раздел, мы хотим разъяснить еще одно обстоятельство.
Исключение конструктора из класса и замена его другим конструктором — весьма грубая и очевидная ошибка. Способен ли механизм контроля версии .NET спасти от других, менее тривиальных ошибок? Да, способен.
Рассмотрим самый распространенный (хотя довольно тривиальный) источник ошибок несовместимости при использовании наследования. Имеется производный класс Derived, зависящий от базового класса Parent. В класс Derived включается новый метод Parselt (в следующем примере он просто разделяет строку по словам и выводит каждое слово в отдельной строке):
Imports Microsoft.VisualBasic.ControlChars Module Modulel
SubMain()
Dim myDerived As New Oerived()
myDerived.DisplayIt 0
Console.ReadLine()
End Sub
End Module
Public Class Parent
Public Const MY STRING As String ="this is a test"
Public Overridable Sub Displaylt()
Console.WriteLine(MY_STRING)
End Sub
End Class
Public Class Derived Inherits Parent
Public Overrides Sub Displaylt()
Console.WriteLine(ParseIt(MyBase.MY_STRING))
End Sub
Public Function ParselUByVal aString As String)
Dim tokens() As String
' Разбить строку по пробелам tokens -
aString.Split(Chr(32))
Dim temp As String
' Объединить в одну строку, вставляя между словами
' комбинацию символов CR/LF
temp = Join(tokens.CrLf)
Return temp
End Function
End Class
End Module
Результат показан на рис. 5.8.
Рис. 5.8. Простейшее разбиение строки по словам
Теперь представьте себе, что класс Parent распространяется не в виде исходных текстов, а в откомпилированной форме. Версия 2 класса Parent содержит собственную версию Parselt, которая широко используется в ее коде. В соответствии с принципом полиморфизма при хранении объекта типа Den ved в объектной переменной типа Parent вызовы Displaylt должны использовать метод Parselt класса Derived вместо метода Parselt базового класса. Однако здесь возникает маловероятная, но теоретически возможная проблема. В нашем сценарии код класса Parent, использующий свою версию функции Parselt, не знает, как функция Parselt реализована в классе Derived.Полиморфный вызов версии Parselt производного класса может нарушить какие-либо условия, необходимые для работы базового класса.
В этой ситуации средства контроля версии VB .NET тоже творят чудеса: код откомпилированного базового класса Parent продолжает использовать свою версию Parselt всегда, даже несмотря на то, что при хранении объектов Derived в переменных типа Parent полиморфизм привел бы к вызову неправильной версии метода. Как упоминалось в предыдущем примере, при открытии кода Derived в Visual Studio компилятор сообщает, что для устранения неоднозначности в объявление метода Parselt производного класса следует включить ключевое слово Override или Shadows.