Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Использование объектно-ориентированного подхода
Инкапсуляцию можно считать одной из наиболее важных концепций объектно- ориентированного программирования. Инкапсуляция — это дисциплина тщательного контроля доступа к внутренним данным и процедурам объектов. Ни один язык, не поддерживающий инкапсуляцию, не может претендовать на звание объектно- ориентированного. Всегда необходимо стараться следовать базовой концепции: никогда не определять поля данных объектов с открытым доступом. Необходимо сделать так, чтобы клиенты объекта общались с ним только управляемым образом. Обычно это означает организацию взаимодействия с объектом только через методы этого объекта (или свойства, которые, по сути, являются вызовами методов). Таким образом, внутренности объекта рассматриваются как " черный ящик". Внутреннее хозяйство объекта внешнему миру не видно, а все коммуникации, которые могут модифицировать внутренности, осуществляются по контролируемым каналам. С помощью инкапсуляции можно спроектировать такой дизайн, который гарантирует, что внутреннее состояние объекта никогда не будет нарушено, что демонстрируется следующим простым примером: class MyRectangle { private uint widths; private uint height; private uint area; public uint Width { get { return width; } set { width = value; ComputeArea(); } } public uint Height { get { return height; } set { height = value; ComputeArea(); } } public uint Area { get { return area; } } private void ComputeArea() { area = width * height; } } При определении классов могут использоваться интерфейсы. Интерфейс — это определение контракта. Классы могут реализовывать разные интерфейсы и за счет этого гарантировать выполнение требований контракта. Когда класс наследуется от интерфейса, он обязан реализовать все члены этого интерфейса. Класс может реализовывать столько интерфейсов, сколько нужно; при этом интерфейсы перечисляются в списке базовых классов определения класса. В общих чертах синтаксис интерфейса очень похож на синтаксис класса. Однако каждый его член неявно имеет модификатор public. Объявление любого члена интерфейса с каким-нибудь явным модификатором приведет к возникновению ошибки времени компиляции. Интерфейсы могут содержать только методы экземпляра; другими словами, включать статические методы в их определения нельзя. Интерфейсы не содержат реализации, т.е. они по природе своей семантически абстрактны. Если вы знакомы с языком C++, то знаете, что там можно создать подобную конструкцию, объявляя класс, который содержит только общедоступные, чистые виртуальные методы, не имеющие реализаций по умолчанию. Элементами интерфейса могут быть методы, свойства, события и индексаторы, которые в конечном итоге становятся методами в CLR. В следующем коде показан пример интерфейса и класса, реализующего интерфейс: public interface IMusic // Примечание: общепринятая практика заключается //в предварении имен интерфейсов заглавной буквой I { void PlayMusic (); ) public class Person: IMusic { public void PlayMusic () {} public void DoALittleDance () {} } public class EntryPoint { static void Main() { Person dude = new Person (); IMusic music = dude; music.PlayMusic (); dude.PlayMusic(); dude.DoALittleDance (); } } Здесь клиенты могут обращаться к методу PlayMusic одним из двух способов. Они могут либо вызывать его непосредственно через экземпляр объекта, либо получить интерфейсную ссылку на экземпляр объекта и вызвать метод через нее. Поскольку класс Person поддерживает интерфейс IMusic ссылки на объекты этого класса являются неявно преобразуемыми в ссылки на IMusic. В коде метода Main в предыдущем примере продемонстрирован вызов методов обоими способами. Большинство разработчиков считают наследование краеугольным камнем объектно-ориентированного программирования. Некоторые склонны считать инкапсуляцию более строгой характеристикой объектно-ориентированного программирования. Наследование — это важная концепция и полезный инструмент. Однако, как и множество других мощных инструментов, в случае неправильного применения оно может представлять опасность. Доступность членов является важным аспектом наследования, особенно, когда речь идет о доступности членов базового класса из производного класса. Любые общедоступные члены базового класса становятся общедоступными и в производном классе. Любые члены, помеченные модификатором protected (защищенные), доступны только внутри объявляющего их класса и его наследников. Защищенные члены никогда не доступны извне определяющего их класса или его наследников. Приватные (private) члены не доступны нигде, кроме определяющего их класса. Поэтому, несмотря на то, что производный класс наследует все члены базового класса, включая и приватные, код в производном классе не имеет доступа к приватным членам, унаследованным от базового класса. Вдобавок защищенные внутренние (protected internal) члены также видимы всем типам, определенным внутри одной сборки, и классам-наследникам определившего их класса. Реальность состоит в том, что производный класс наследует все члены базового класса за исключением конструкторов экземпляра, статических конструкторов и деструкторов. Как было показано, управлять доступностью всего класса в целом можно при его определении. Единственными вариантами доступа к типу класса являются internal и public. При использовании наследования действует правило, что тип базового класса должен быть доступен как минимум настолько же, как и производный класс. Рассмотрим следующий код: class A { protected int x; } public class В: А { } Этот код не скомпилируется, потому что класс А объявлен как internal и не является настолько (как минимум) доступным, как производный от него класс В. Вспомните, что в отсутствие модификатора доступа класс имеет доступ internal, поэтому класс А на самом деле является internal. Для того чтобы этот код компилировался, понадобится либо повысить класс А до уровня доступа public, либо ограничить класс В доступом internal. К тому же обратите внимание, что для класса А допустимо быть public, a для класса В — internal. Наследование позволяет позаимствовать реализацию. Другими словами, можно унаследовать класс D от класса А и повторно использовать реализацию класса А в классе D. Потенциально это позволит сэкономить некоторую часть работы при определении класса D. Другое применение наследования — специализация, когда класс D становится специализированной формой класса А. Пусть классы Rectangle и Circle наследуются от класса GeometricShape. Друими словами, они являются специализациями класса GeometricShape. Специализация бессмысленна без полиморфизма и виртуальных методов. Полиморфизм описывает ситуацию, когда тип, на который ссылается определенная переменная, может вести себя как (и в действительности быть) экземпляр другого (более специализированного) типа. Такую модель можно реализовать следующим образом: public class GeometricShape { public virtual void Draw() { // Выполнить некоторое рисование по умолчанию } } public class Rectangle: GeometricShape { public override void Draw() { // Нарисовать прямоугольник } } public class Circle: GeometricShape { public override void Draw() { // Нарисовать круг } } public class EntryPoint { private static void DrawShape( GeometricShape shape ) { shape.Draw(); } static void Main() { Circle circle = new Circle(); GeometricShape shape = circle; DrawShape( shape ); DrawShape( circle ); } } В методе Main создается новый экземпляр Circle. Сразу после этого получается ссылка типа GeometricShape на тот же объект. Это важный момент. Компилятор здесь неявно преобразует эту ссылку в ссылку на тип GeometricShape, позволяя использовать простое выражение присваивания. На самом деле, однако, она продолжает ссылаться на тот же объект Circle. В этом суть специализации типа и автоматического преобразования, сопровождающего ее. Теперь рассмотрим оставшуюся часть кода метода Main. После получения ссылки GeometricShape на экземпляр Circle можно передать ее методу DrawShape, который не делает ничего кроме вызова метода Draw переданной ему фигуры. Однако ссылка на объект фигуры на самом деле указывает на Circle, метод Draw определен как виртуальный, а класс Circle переопределяет виртуальный метод, так что вызов Draw на ссылке GeometricShape на самом деле приводит к вызову Circle. Draw. Это и есть полиморфизм в действии. Метод DrawShape не интересует, какой конкретный тип фигуры представляет переданный ему объект. То, с чем он имеет дело — это GeometricShape, a Circle является типом GeometricShape. Вот почему наследование иногда называют отношением " is-a" (" является" ). В данном примере Rectangle является GeometricShape и Circle является GeometricShape. Ключ к ответу на вопрос, когда наследование имеет смысл, а когда нет, лежит в применении отношения " is-a" к существующему дизайну. Если класс D наследуется от класса В, и класс D семантически не является классом В, то для данного отношения наследование является неподходящим инструментом. Следует дать еще одно важное замечание по поводу наследования и возможности преобразования. Выше упоминалось, что компилятор неявно преобразует ссылку на экземпляр Circle в ссылку на экземпляр GeometricShape. Неявно в данном случае означает, что код не должен делать ничего специального для выполнения такого преобразования, а под " чем-то специальным" обычно имеется в виду операция приведения. Поскольку компилятор обладает способностью делать это на основе знания иерархии наследования, то может показаться, что можно и обойтись без получения ссылки на GeometricShape перед вызовом DrawShape с экземпляром Circle. На самом деле так оно и есть! Это доказывает последняя строка метода Main. Ссылку на экземпляр Circle можно просто передать непосредственно методу DrawShape, и поскольку компилятор может неявно преобразовать ее в ссылку на тип GeometricShape исходя из отношений наследования, он выполнит всю работу за вас. Здесь снова проявляется вся мощь этого механизма. При наследовании класса в методе производного класса часто возникает необходимость вызова метода либо доступа к полю, свойству или индексатору базового класса. Для этой цели предусмотрено ключевое слово base. Это ключевое слово можно применять подобно любой другой переменной экземпляра, но его можно использовать только внутри блока конструктора экземпляра, метода экземпляра или средства доступа к свойству. Применение его в статических методах не допускается. Это совершенно оправдано, потому что base открывает доступ к реализациям экземпляра базового класса, подобно тому, как this разрешает доступ к экземпляру — владельцу текущего метода. Рассмотрим следующий блок кода: public class A { public A( int var ) { this.x = var; } public virtual void DoSomething () { System.Console.WriteLine ( " A.DoSomething" ); } private int x; } public class В: А { public В () : base( 123 ) { } public override void DoSomething() { System.Console.WriteLine ( " B.DoSomething" ); base.DoSomething(); } } public class EntryPoint { static void Main() { В b = new В (); b.DoSomething (); } } В этом примере продемонстрированы два применения ключевого слова base, и одно из них — в конструкторе класса В. Напомним, что класс не наследует конструкторы экземпляра. Однако при инициализации объекта иногда требуется явно вызвать один из конструкторов базового класса во время инициализации производного класса. Это объясняет нотацию, примененную в конструкторе экземпляра класса В. Инициализация базового класса происходит после объявления списка параметров конструктора производного класса, но перед блоком кода конструктора производного класса. Второе применение ключевого слова base содержится в реализации В. DoSomething. Было принято решение, что при реализации метода DoSomething в классе В необходимо позаимствовать реализацию DoSomething из класса А. Реализацию A. DoSomething можно вызвать непосредственно из реализации В. DoSomething, снабдив вызов префиксом — ключевым словом base. В С# предлагается ключевое слово sealed для тех случаев, когда нужно сделать так, чтобы клиент не мог наследовать свой класс от конкретного класса. Примененное к целому классу, ключевое слово sealed указывает на то, что данный класс является листовым в дереве наследования. Под этим понимается запрет наследования от данного класса. Если диаграмма наследования представлена в виде деревьев, то sealed-классы имеет смысл называть листовыми или герметизированными. Поначалу может показаться, что ключевое слово sealed придется использовать редко. Однако на самом деле должно быть наоборот. При проектировании новых классов это ключевое слово должно применяться настолько часто, насколько возможно. Многие советуют использовать его по умолчанию. Абстрактные классы диаметрально противоположны классам sealed. Иногда необходимо спроектировать класс, единственное назначение которого — служить базовым классом. Подобного рода классы помечаются ключевым словом abstract. Ключевое слово abstract сообщает компилятору, что назначение данного класса — служить базовым, и потому создавать экземпляры этого класса не разрешено.
Популярное:
|
Последнее изменение этой страницы: 2016-05-03; Просмотров: 619; Нарушение авторского права страницы