ние фарами оказалось там, где у всех "нормальных" автомобилей находится управление сигналами поворота. Отличие вроде бы небольшое, но я так и не научился поворачивать на этом автомобиле влево, не выключив при этом фары...
Кроме того, при хорошо продуманном дизайне автомобиля один и тот же элемент управления никогда не будет использоваться для выполнения более одной операции в за висимости от состояния автомобиля.
Микроволновая печь должна быть сконструирована таким образом, чтобы никакая комбинация кнопок или рукояток не могла ее сломать или навредить вам. Конечно, оп ределенные комбинации не будут выполнять никаких функций, но главное, чтобы ни од на из них не привела к следующему.
Поломке устройства. Какие бы рукоятки ни крутил ваш ребенок и какие бы кнопки не нажимал — микроволновая печь не должна от этого сломаться. После того как вы вернете все элементы управления в корректное состояние, она должна нормально работать.
К пожару или прочей порче имущества или нанесению вреда здоровью по требителя. Мы живем в сутяжном мире, и если бы что-то похожее могло про изойти — компании пришлось бы продать все вплоть до автомобиля ее президен та, чтобы рассчитаться с подающими на нее в суд и адвокатами.
Однако чтобы эти два правила выполнялись, вы должны принять на себя определен ную ответственность. Вы ни в коем случае не должны вносить изменения в устройство, в частности, отключать блокировки.
Почти все кухонное оборудование любой степени сложности, включая микроволно вые печи, имеет пломбы, препятствующие проникновению пользователя внутрь. Если такая пломба повреждена, это указывает, что крышка устройства была снята, и вся от ветственность с производителя тем самым снимается. Если вы каким-либо образом из менили внутреннее устройство печи, вы сами несете ответственность за все последую щие неприятности, которые могут произойти.
Аналогично, класс должен иметь возможность контролировать доступ к своим членамданным. Никакая последовательность вызовов членов класса не должна приводить программу к аварийному завершению, однако класс не в состоянии гарантировать это, если внешние объекты имеют доступ к внутреннему состоянию класса. Класс должен иметь возможность прятать критические члены-данные и делать их недоступными для внешнего мира.
Итак, как же С# реализует объектно-ориентированное программирование? Впрочем, это не совсем корректный вопрос. С# является объектно-ориентированным языком про-
Глава 10. Что такое объектно-ориентированное программирование |
229 |
граммирования, но не реализует его — это делает программист. Как и на любом другом гом языке, вы можете написать на С# программу, не являющуюся объект! ориентированной (например, вставив весь код Word в функцию M a i n O ) . Иногда нужно писать и такие программы, но все же главное предназначение С# — создает объектно-ориентированных программ.
С# предоставляет программисту следующие необходимые для написания объект! ориентированных программ возможности.
Управляемый доступ. С# управляет обращением к членам класса. Ключе» слова С# позволяют объявить некоторые члены открытыми для всех, а другие защищенными или закрытыми. Подробнее эти вопросы рассматриваются в гла ве 11, "Классы".
Специализация. С# поддерживает специализацию посредством механизма, i вестного как наследование классов. Один класс при этом наследует члены друга класса. Например, вы можете создать класс Саг, как частный случай класса Vi h i c l e . Подробнее эти вопросы рассматриваются в главе 12, "Наследование".
Полиморфизм. Эта возможность позволяет объекту выполнить операцию так, как это требуется для его корректного функционирования. Например, класс Rocket унаследованный от V e h i c l e , может реализовать операцию S t a r t совершенно иначе, чем Саг, унаследованный от того же V e h i c l e . По крайней мере, будем надеяться, что это справедливо хотя бы по отношению к вашему автомобилю хотя с некоторыми автомобилями никогда ни в чем нельзя быть уверенным...| просьг полиморфизма рассматриваются в главах 13, "Полиморфизм", 1 "Интерфейсы и структуры".
230 |
Часть IV. |
Объектно-ориентированное программировал |
Глава 11
Классы
У Защита класса посредством управления доступом
>Инициализация объекта с помощью конструктора
>Определение нескольких конструкторов в одном классе
>Конструирование статических членов и членов класса
ласе должен сам отвечать за свои действия. Так же как микроволновая печь не должна вспыхнуть, объятая пламенем, из-за неверного нажатия кнопки, так и класс не должен скончаться (или прикончить программу) при предоставлении не
корректных данных.
Чтобы нести ответственность за свои действия, класс должен убедиться в корректно сти своего начального состояния и в дальнейшем управлять им так, чтобы оно всегда ос тавалось корректным. С# предоставляет для этого все необходимое.
Простые классы определяют все свои члены как p u b l i c . Рассмотрим программу BankAccount, которая поддерживает член-данные b a l a n c e для хранения информа ции о балансе каждого счета. Сделав этот член p u b l i c , вы допускаете любого в святая святых банка, позволяя каждому самому указывать сумму на счету.
Неизвестно, в каком банке храните свои сбережения вы, но мой банк и близко не на столько открыт и всегда строго следит за моим счетом, самостоятельно регистрируя ка ждое снятие денег со счета и вклад на счет. В конце концов, это позволяет уберечься от всяких недоразумений, если вас вдруг подведет память.
Управление доступом дает возможность избежать больших и малых ошибок в работе банка. Обычно программисты, привыкшие к функциональному про граммированию, говорят, что достаточно лишь определить правило, согласно которому никакие другие классы не должны обращаться к члену b a l a n c e не посредственно. Увы, теоретически это, может быть, и так, но на практике такой подход никогда не работает. Да, программисты начинают работу, будучи пере полненными благими намерениями, которые вскоре непонятно куда исчезают под давлением сроков сдачи проекта...
Пример программы с использованием открытых членов
В приведенной демонстрационной программе класс BankAccount объяв ляет все методы как public, в то же время члены-данные nAccountNumber и dBalance сделаны private. Эта демонстрационная программам
корректна и не будет компилироваться, так как создана исключительно в дидактических целях.
// |
BankAccount - создание банковского счета с использованием |
// |
переменной типа double для хранения баланса |
счета (она |
// |
объявлена как |
p r i v a t e , чтобы скрыть баланс |
от |
внешнего |
// |
мира) |
|
|
|
|
// |
П р и м е ч а н и е : пока в программу не будут внесены |
|
// |
исправления, |
она не будет |
компилироваться, |
так |
как |
// |
функция Main() обращается |
к private - член у класса |
// B a n k A c c o u n t . usin g System;
namespace BankAccount
{
public class Program
{
public static v o i d Main(string[] args)
{
Console . WriteLine("В текущем состоянии эта " + "программа не к о м п и л и р у е т с я . " ) ;
//Открытие банковского счета
Console . WriteLine("Создание объекта " +
|
"банковского |
с ч е т а " ) ; |
BankAccount ba = n e w B a n k A c c o u n t ( ) ; |
b a . I n i t B a n k A c c o u n t ( ) ; |
|
// |
Обращение к балансу при помощи метода Deposit() |
// |
вполне к о р р е к т н о ; Deposit() |
имеет право доступа ко |
//всем членам - данным b a . D e p o s i t ( 1 0 ) ;
//Непосредственное обращение к члену - данны м вызывает
//ошибку компиляции
Console . WriteLine("Здесь вы получите " + "ошибку к о м п и л я ц и и " ) ;
ba . dBalance += 1 0 ;
// Ожидаем подтверждения пользователя Console . WriteLine("Нажмите <Enter> для " +
"завершения программы .. . ") ;
C o n s o l e . R e a d ( ) ;
} }
//BankAccount - определение класса, представляющего
//простейший банковский счет
public class BankAccount
232 |
Часть |
IV. |
Объектно-ориентированное |
программирование |
private static int n N e x t A c c o u n t N u m b e r = 10 0 0 ; private int n A c c o u n t N u m b e r ;
//хранение баланса в виде одной переменной типа double private double d B a l a n c e ;
//Init - инициализация банковского счета с нулевым
//балансом и использованием очередного глобального
//номера
public void InitBankAccount()
{
nAccountNumber = + + n N e x t A c c o u n t N u m b e r ; dBalance = 0.0;
// GetBalance - получение текущего баланса public double GetBalance()
{
return d B a l a n c e ;
// Номер счета
public int GetAccountNumber()
{
return n A c c o u n t N u m b e r ;
}
public void SetAccountNumber(int nAccountNumber)
{
this.nAccountNumber = n A c c o u n t N u m b e r ;
// Deposit - позволен любой положительный вклад public void D e p o s i t ( d o u b l e dAmount)
{
if (dAmount > 0.0)
{
dBalance += d A m o u n t ;
// Withdraw - вы можете |
снять со счета любую сумму, не |
// превышающую б а л а н с ; |
функция возвращает реально снятую |
// сумму |
|
public double W i t h d r a w ( d o u b l e dWithdrawal)
{
if (dBalance <= dWithdrawal)
{
dWithdrawal = d B a l a n c e ;
dBalance -= d W i t h d r a w a l ; return d W i t h d r a w a l ;
private static int n N e x t A c c o u n t N u m b e r = 1 0 0 0 ; private int nAccountNuraber;
//хранение баланса в виде одной переменной типа double private double d B a l a n c e ;
//Init - инициализация банковского счета с нулевым
//балансом и использованием очередного глобального
//номера
public v o i d InitBankAccount()
nAccountNumber = + + n N e x t A c c o u n t N u m b e r ; dBalance = 0.0;
}
// GetBalance - получение текущего баланса public double GetBalance()
return d B a l a n c e ;
// Номер счета
public int GetAccountNumber( )
return n A c c o u n t N u m b e r ;
public void SetAccountNumber(in t nAccountNumber)
this . nAccountNumber = n A c c o u n t N u m b e r ;
// Deposit - позволен любой положительный вклад public void D e p o s i t ( d o u b l e dAmount)
{
if (dAmount > 0.0)
{
dBalance += d A m o u n t ;
// |
Withdraw - |
вы можете |
снять |
со счета любую сумму, не |
// |
превьшающую |
б а л а н с ; |
функция |
возвращает реально снятую |
// |
сумму |
|
|
|
public double W i t h d r a w ( d o u b l e dWithdrawal) |
if (dBalance |
<= dWithdrawal) |
|
dWithdrawal = d B a l a n c e ;
dBalance -= d W i t h d r a w a l ; return d W i t h d r a w a l ;
233
// GetString |
- возвращает информацию о состоянии счета в |
// - виде |
строки |
public string |
GetString() |
{ |
|
|
string |
s = |
String . Format("#{0} = { l : C } " , |
|
|
G e t A c c o u n t N u m b e r ( ) , |
|
|
G e t B a l a n c e ( ) ) ; |
retur n |
s; |
|
В этом коде выражение dBalance -= dWithdrawal означает то же, чтя
и dBalance = dBalance - dWithdrawal . Обычно программисты на C# стараются использовать наиболее короткую запись из возможных.
Объявляя член как public, вы делаете его доступным для любого кода вашей про граммы.
Класс BankAccount предоставляет метод InitBankAccount () для инициализа-1 ции членов класса, метод Deposit () — для обработки вкладов на счет и метод With-I draw ( ) — для снятия денег со счета. Методы Deposit ( ) и Withdra w ( ) даже обес-1 печивают выполнение некоторых рудиментарных правил — "нельзя вкладывать отрица-1 тельные суммы" и "нельзя снимать больше, чем есть на счету". Однако в открытой! системе, где член-данные dBalance доступен для внешних методов (под внешнтш подразумеваются методы "в пределах той же программы, но внешние по отношению! к классу"), эти правила могут быть нарушены кем угодно. Особенно существенной про-1 блемой это может оказаться при разработке больших проектов группами программистов.! Это может стать проблемой и для одного человека, поскольку ему свойственно ошибать-1 ся. Хорошо спроектированный код с правилами, выполнение которых проверяет компи-1 лятор, значительно снижает количество источников возможных ошибок.
Перед тем как идти дальше, обратите внимание, что приведенная демонстрационная про-1 грамма не будет компилироваться — при такой попытке вы получите сообщение о том, что! обращение к члену DoubleBankAccount. BankAccount. dBalance невозможно:
'DoubleBankAccount . BankAccount . dBalance' is inaccessible due to its p r o t e c t i o n level .
Трудно сказать, зачем компилятор заставили выводить такие скучные сообщения вместо короткого "не лезь к private", но суть именно в этом. Выражение ba . dBalance += 10; I оказывается некорректным именно по этой причине— в силу объявления dBalance как] private этот член недоступен виртуальной функции Main ( ) , расположенной вне класса BankAccount. Замена данного выражения на ba.Deposit (10) решает возникшую про блему— метод BankAccount. Deposit () объявлен как public, а потому доступен для функции Main ().
Тип доступа по умолчанию — private, так что если вы забыли или созна тельно пропустили модификатор для некоторого члена — это аналогично тому, как если бы вы описали его как private. Однако настоятельно рекомендуется всегда использовать это ключевое слово явно во избежание любых недоразу
мений. Хороший программист всегда явно указывает свои намерения, что является еще одним методом снижения количества возможных ошибок.
234 |
Часть |
IV. |
Объектно-ориентированное |
программирование |
Прочие уровни безопасности
Вэтом разделе используются определенные знания о наследовании и про странствах имен, которые будут рассмотрены в более поздних главах книги. Вы можете сейчас пропустить настоящий раздел и вернуться к нему позже, по лучив необходимые знания.
С# предоставляет следующие уровни безопасности.
Члены, объявленные как public, доступны любому классу программы. Члены, объявленные как private, доступны только из текущего класса.
Члены, объявленные как protected, доступны только из текущего клас са и всех его подклассов.
Члены, объявленные как internal, доступны для любого класса в том же модуле программы.
Модулем в С# называется отдельно компилируемая часть кода, представ ляющая собой выполнимую . ЕХЕ-программу либо библиотеку . DLL. Од но пространство имен может распространяться на несколько модулей.
Члены, объявленные как internal protected, доступны для текуще го класса и всех его подклассов в том же модуле программы.
Скрытие членов путем объявления их как privat e обеспечивает максимальную степень безопасности. Однако зачастую такая высокая степень и не нужна. В конце концов, шы подклассов и так зависят от членов базового класса, так что ключевое слово p r o jected предоставляет достаточно удобный уровень безопасности.
I Объявление внутренних членов класса как public — не лучшая мысль как минимум
по следующим причинам.
Объявляя члены-данные public , вы не в состоянии просто определить, когда и как они модифицируются. Зачем беспокоиться и создавать методы Deposit () и W i t h d r a w () с проверками корректности? И вообще, зачем соз давать любые методы — ведь любой метод любого класса может модифициро вать данные счета в любой момент. Но если другая функция может обращаться к этим данным, то она практически обязательно это сделает.
Ваша программа BankAccoun t может проработать длительное время, прежде чем вы заметите, что баланс одного из счетов— отрицателен. Метод With draw () призван оградить от подобной ситуации, но в описанном случае непо средственный доступ к балансу, минуя метод W i t h d r a w n , имеют и другие функции. Вычислить, какие именно функции и при каких условиях поступают так некорректно — задача не из легких.
Доступ ко всем членам-данным класса делает его интерфейс слишком слож ным. Как программист, использующий класс BankAccount, вы не хотите знать о том, что делается внутри него. Вам достаточно знаний о том, как положить деньги на счет и снять их с него.
Доступ ко всем членам-данным класса приводит к "растеканию" права класса. Например, класс BankAccount не позволяет балансу стать отрицата ным ни при каких условиях. Это — бизнес-правило, которое должно быть локал зовано в методе Withdra w ( ) . В противном случае вам придется добавлять ш ветствующую проверку в весь код, в котором осуществляется изменение баланса,
Что произойдет, когда банк решит изменить правила, и часть клиентов с хорош кредитной историей получит право на небольшой отрицательный баланс в тече! короткого времени? Вам придется долго рыскать по всей программе и вноси изменения во все места, где выполняется непосредственное обращение к балансу,
Не делайте классы и методы более доступными, чем это необходимо. Этон параноидальная боязнь хакеров — это просто поможет вам снизить количесп ошибок в коде. По возможности используйте модификатор private, а зате при необходимости поднимайте его до protected, internal, internal protecte d или public.
Методы доступа
Если вы более внимательно посмотрите на класс BankAccount, то увидите несколь ко других методов. Один из них, GetStrin g ( ) , возвращает строковую версию счел для вывода ее на экран посредством функции Console . WriteLin e ( ) . Дело в том, чл вывод содержимого объекта BankAccount может быть затруднен, если это содержим» недоступно. К тому же, следуя принципу "отдайте кесарю кесарево", класс должен ими право сам решать, как он будет представлен при выводе.
Кроме того, имеется один метод для получения значения — GetBalance () и набо] методов для получения и установки значения — GetAccountNumbe r () и SetAc countNumber ( ) . Вы можете удивиться — зачем так волноваться из-за того, чтоб! член dBalance был объявлен как private, и при этом предоставлять метод GetBal апсе () ? На самом деле для этого имеются достаточно веские основания.
GetBalance О не дает возможности изменять член dBalance — он тольк возвращает его значение. Тем самым значение баланса делается доступны только для чтения. Используя аналогию с настоящим банком, вы можете просмот реть состояние своего счета в любой момент, но не можете снять с него деньги иначе, чем с применением процедур, предусмотренных для этого банком.
Метод GetBalance () скрывает внутренний формат класса от внешних ме тодов. Метод GetBalanc e () может в процессе работы выполнять некоторые вычисления, обращаться к базе данных банка — словом, выполнять какие-то дей ствия, чтобы получить состояние счета. Внешние функции ничего об этом не зна ют и не должны знать. Продолжая аналогию, вы интересуетесь состоянием счета но не знаете, как, где и в каком именно виде хранятся ваши деньги.
И наконец, метод GetBalanc e () предоставляет механизм для внесения внутрення! изменений в класс BankAccount, абсолютно не затрагивая при этом его пользователе!,! Если от нацбанка придет распоряжение хранить деньги как-то иначе, это никак не долх-1 но сказаться на вашем способе обращения с вашим счетом (по крайней мере так должно! быть в цивилизованном обществе).
236 |
Часть IV. |
Объектно-ориентированное программирован |
Пример управления доступом
Приведенная далее демонстрационная программа DoubleBankAccount указывает потенциальные изъяны программы BankAccount .
// |
DoubleBankAccount |
- с о з д а н и е |
б а н к о в с к о г о |
с ч е т а |
с |
/ / |
и с п о л ь з о в а н и е м п е р е м е н н о й т и п а double д л я |
х р а н е н и я |
|
баланса |
с ч е т а ( о н а |
о б ъ я в л е н а |
к а к private , |
ч т о б ы |
с к р ы т ь |
// |
баланс |
от в н е ш н е г о |
м и р а ) |
|
|
|
using System;
namespace DoubleBankAccount
{
{
public class Program |
|
|
public static v o i d Main(string[] |
args) |
{ |
|
|
/ / О т к р ы т и е б а н к о в с к о г о с ч е т а |
|
C o n s o l e . W r i t e L i n e ( "Создани е |
о б ъ е к т а " + |
" б а н к о в с к о г о с ч е т а " ) ; |
BankAccount ba = n e w B a n k A c c o u n t ( ) ; |
b a . I n i t B a n k A c c o u n t ( ) ; |
|
|
/ / В к л а д н а с ч е т |
|
|
double dDeposit = 1 2 3 . 4 5 4 ; |
|
|
C o n s o l e . W r i t e L i n e ( " В к л а д { 0 : C } " , d D e p o s i t ) ; |
b a . D e p o s i t ( d D e p o s i t ) ; |
|
|
/ / Б а л а н с с ч е т а |
{ о } " , |
ba . GetString()) ; |
C o n s o l e . W r i t e L i n e ( " С ч е т = |
//Вот где имеетс я неприятность
double dAdditio n = |
0.002; |
|
C o n s o l e . W r i t e L i n e ( " В к л а д { 0 : C } " , d A d d i t i o n ) ; |
b a . D e p o s i t ( d A d d i t i o n ) ; |
|
|
/ / Р е з у л ь т а т |
|
|
|
Console . WriteLine( "В |
р е з у л ь т а т е с ч е т |
= { о } " , |
|
b a . G e t S t r i n g ( ) ) ; |
|
/ / Ожидаем |
п о д т в е р ж д е н и я |
п о л ь з о в а т е л я |
C o n s o l e . W r i t e L i n e ( "Нажмит е <Enter> |
д л я " + |
|
|
" з а в е р ш е н и я п р о г р а м м ы . . . " ) ; |
C o n s o l e . R e a d ( ) ; |
|
|
|
} |
|
|
|
|
} |
|
|
|
|
/ / BankAccount |
- о п р е д е л е н и е |
к л а с с а , п р е д с т а в л я ю щ е г о |
/ / п р о с т е й ш и й б а н к о в с к и й с ч е т public class BankAccount
{
private static int n N e x t A c c o u n t N u m b e r = 1 0 0 0 ; private int n A c c o u n t N u m b e r ;