Неформальное введение в объектно-ориентированное программирование на платформе .NET Framework 3

Неформальное введение в объектно-ориентированное программирование на платформе .NET Framework

Продолжение. Начало в КГ №№ 13, 14

Предисловие
В прошлой статье цикла я вам рассказывал о недостатках линейного подхода к программированию. В этой статье мы рассмотрим, как ООП позволяет обойти эти недостатки. Честно говоря, я первоначально собирался рассмотреть этот вопрос чисто теоретически, не привлекая в качестве примеров тексты программ. Но в процессе написания статьи я поймал себя на том, что откровенно мямлю и оперирую придуманными мной же самим невнятными терминами. Я подумал и решил несколько забежать вперед и продемонстрировать основные концепции ООП с помощью примеров на C#. Не отчаивайтесь, если некоторые синтаксические конструкции моих примеров останутся для вас непонятны. Позже мы рассмотрим их подробно. Сейчас для вас главное ухватить саму идею программирования в стиле ООП.

Альтернатива процедурному подходу — объектно-ориентированное программирование (ООП)
Очень многие программисты, пишущие свои программы на C++, Delphi и Visual Basic, прочитав заголовок этой главы, недоуменно пожмут плечами: "Эка невидаль! Тоже мне открыл Америку! Мы и так постоянно пользуемся ООП в своих программах!" Дело в том, что "пользоваться ООП" и "писать объектно-ориентированные программы" — это совершенно разные вещи. Объектно-ориентированное программирование требует особого подхода еще на этапе проектирования алгоритма того, что вы собираетесь описать в его терминах.
Для начала давайте рассмотрим, чем вообще ООП-подход к программированию отличается от процедурного подхода. ООП не оперирует раздельными понятиями данных и методов. В нем и данные, и методы для их обработки объединяются в единую сущность, называемую объектом.
Разберем это отличие на нашем примере алгоритма приготовления кофе, описанного в предыдущей главе. Начиная создавать программу, сразу следует разобрать и описать все сущности, участвующие в вашем алгоритме. Поехали:
(1) Есть такой вид объектов, как исходный продукт для приготовления напитков. Он может требовать предварительной обработки перед использованием или не требовать подобной обработки.
(2) Частным случаем такого вида объектов являются зерна кофе. Они бывают измельченные или нет. Если они еще не измельчены, то перед приготовлением напитка их требуется измельчить.
(3) Есть такой вид объектов, как агрегаты для предварительной подготовки исходных продуктов. Конкретный тип объекта обрабатывает конкретный вид продукта.
(4) Частным случаем такого вида объектов является кофемолка. Она предназначена для измельчения кофейных зерен.
(5) Есть такой тип объектов, как сосуд для приготовления напитков. За некоторыми из этих объектов нужно следить во время работы, а за некоторыми — нет. Каждый сосуд предназначен для своего вида исходных продуктов.
(6) Частным случаем такого типа объектов является турка. Она служит для варки помолотых кофейных зерен и требует, чтобы за ней приглядывали во время процесса работы. Если за ней не следить, кофе убегает. В турку следует добавлять горячую воду.
(7) Есть такой тип объектов, как агрегат для приготовления напитков. Любой из них умеет включаться и выключаться. Каждый объект этого типа умеет тем или иным способом сигнализировать об окончании своей работы.
(8) Частным случаем такого типа объектов является газовая плита. Она сигнализирует об окончании процесса варки тем, что кофе готовится убегать.
(9) Да, вот еще, чуть не забыл! Нам же требуется еще один ингредиент для приготовления кофе: это вода. Вода в нашем алгоритме является частным видом исходного продукта. Вода бывает горячая или холодная.

Разобрав участвующие в алгоритме объекты, можно приступать к описанию их взаимных связей. Для этого давайте создадим еще один вспомогательный объект. Назовем его "бармен". Объясняем нашему начинающему бармену процесс приготовления любых(!) произвольных напитков.
Говорим ему, что он должен взять исходные продукты (номер 1 в предыдущем списке объектов). Заметьте: именно "1", а не "2"!!! Внимательно следите за цифрами в этом примере, а не то упустите самое интересное. Взяв продукты, следует выяснить, требуют они предварительной обработки или нет.
Если продукты требуют обработки, он должен взять агрегат для предварительной подготовки продуктов (3) и обработать в нем продукты.
Затем бармен должен взять сосуд для приготовления напитков (5) и заложить в него продукты.
Установить сосуд (5) на агрегат для приготовления напитков (7) и включить его. Дождавшись, когда агрегат доложит о готовности напитка, выключить его.
Все. Напиток готов.
Ну вот, подготовив бармена к выполнению его функциональных обязанностей, наконец-то приступаем к написанию основного потока вычислений нашей программы.
Говорим бармену, что исходным продуктом номер раз будут зерна кофе (2).
Говорим бармену, что исходным продуктом номер два будет вода (9).
В качестве агрегата для предварительной обработки следует взять кофемолку (4).
Сосудом для приготовления кофе будет турка (6).
В качестве агрегата для приготовления напитка выступает газовая плита (8).
Бармен — вперед! Я жду свою утреннюю чашечку кофе!
Вот и весь наш алгоритм, выдержанный в объектно-ориентированном стиле. Первое, что наверняка вам бросится в глаза, это сравнительно больший объем предварительной работы, которую необходимо проделать, прежде чем мы начинаем программировать задачу. Многих программистов старой школы это обстоятельство отпугивает от использования ООП. "Вот еще! — говорят они. — Не стану я ради написания несложной программы городить весь этот огород! И упрямо пишут раз за разом одни и те же несложные программы, а затем постоянно их ковыряют, когда клиент просит изменить один или два незначительных аспекта в окончательном продукте.

Простота модификации имеющегося кода
Давайте на примере с заменой турки и газовой плиты на кофеварку проследим, какие изменения нам придется внести в свое приложение варки кофе.
Создаем еще один частный случай исходного продукта вода (9). Говорим, что воду греть не надо.
Создаем еще один частный случай сосуда для приготовления напитков — емкость для воды в кофеварке. Говорим, что за ней не надо следить, и принимает она только холодную воду.
Создаем частный случай агрегата для приготовления напитков — кофеварка. Указываем, что об окончании процесса сигнализирует срабатывание включателя сети.
На этом подготовительная часть модификации программы окончена. Теперь давайте научим нашего бармена варить кофе в кофеварке.
Говорим бармену, что исходным продуктом номер раз будут зерна кофе.
Говорим бармену, что исходным продуктом номер два будет холодная вода.
В качестве агрегата для предварительной обработки следует взять кофемолку.
Сосудом для приготовления кофе будет емкость для воды в кофеварке.
В качестве агрегата для приготовления напитка выступает кофеварка.
Бармен — вперед! Я жду свою утреннюю чашечку кофе!
Вот и все изменения. Теперь наш бармен умеет варить кофе и в кофеварке, и с помощью турки на газовой плите. Прониклись идеей? Обратите внимание: модифицируя прежнюю версию программы, мы нигде ничего не исправляем. Мы просто добавляем новую или изменившуюся функциональность. При этом мы вовсе ничего не меняли в коде "бармена", так как сама логическая последовательность приготовления напитка не изменилась — изменились лишь ингредиенты и используемая аппаратура.

Использование данных и методов не по назначению
Теперь давайте посмотрим, а сможем ли мы "завалить" наш алгоритм, подсунув ему неверный тип данных. Для примера возьмем в качестве исходного продукта "турку" и попробуем смолоть ее в "кофемолке".
О-о-опс! А не получается! Во-первых, у нас вообще нет такого типа исходного продукта, во-вторых — "кофемолка" просто не умеет молоть "турку". У нее нет для этого алгоритма! Таким образом, наша программа изначально защищена от неправильного использования типов данных. При этом "защита от дурака" происходит совершенно естественным образом. Никто ничего никому не запрещает, и нигде в коде нет никаких проверок на правильность данных. Неправильное использование данных просто логически и физически невозможно.
Если вы, злобный хакер, возьмете и опишете продукт "турку", вам дополнительно придется обучать агрегат для измельчения ее молоть, а агрегат для варки — ее варить. То есть фактически придется создать новую программу, которую вы можете и "ломать" в свое удовольствие. Но моя исходная программа от этого ничуть не пострадает и будет работать точно так же, как и раньше, не подпуская турку к кофемолке и на пушечный выстрел.

Наглядность исходного алгоритма в тексте программы
Помните, в предыдущей статье я вам описывал проблемы, возникающие от наличия большого количества сущностей в программе? Так вот, ООП решает и эту проблему. Обратите внимание: программируя ту или иную сущность, мы имеем дело только с ней самой и ни с чем другим.
К примеру, когда мы описываем зерна кофе, нам совершенно безразлично, что дальше происходит с ними в программе. Собираемся ли мы их впоследствии молоть, варить или подбрасывать вверх, а затем ловить, нас пока не интересует. Мы просто говорим, что зерна могут вариться и молоться с помощью неких абстрактных "агрегатов", до конкретных реализаций которых нам, в данную единицу времени, нет никакого дела. Мы просто указываем, что такое действие в принципе возможно — и все.
Описав таким образом зерна кофе, мы точно так же можем забыть о том, что они из себя конкретно представляют. В дальнейшем они для нас просто один из "исходных продуктов", обладающих базовыми для такого рода объектов свойствами.
Таким образом, "зараз" мы оперируем лишь небольшим количеством объектов в программе, абстрагируясь от конкретики реализации всех остальных.

Побочным результатом такого "абстрагирования" используемых сущностей является и большая наглядность самого исходного алгоритма. Он получается меньше перегруженным частностями реализации конкретных объектов.
Давайте приготовимся создать нашу первую объектно-ориентированную программу. Запускаем Visual Studio, выбираем File-> New-> Project. Ждем, пока загрузится мастер создания нового проекта. Выбираем в дереве слева Visual C# Projects. В списке справа выбираем Windows Appli-cation. Внизу есть два поля ввода. В поле Name указываете желаемое название вашего проекта (именно оно и станет названием исполнимого файла). Назовем наш проект "Sample1". В поле Location укажите папку, в которой вы хотите создать новый проект. Я буду использовать папку GI_Samples. Жмете кнопку OK.
И вправду "OK"! Через незначительное время ваш первый проект будет создан. Пока вы оглядываете возникшее перед вами великолепие, я поясню остальным читателям, у которых нет Visual Studio, как им создать проект без ее участия. Просто заведите, где-нибудь на винчестере, папку GI_Samples. В этой папке делаете подкаталог Sample1. Запустите свой любимый текстовый редактор и создайте в папке Sample1 файл под названием Form1.cs. Также сделайте файл compile.bat, который будет заниматься у вас сборкой проекта. Содержимое файла compile.bat выглядит следующим образом:

@Echo off
csc.exe /out:Sample1 /t:exe Form1.cs

Вот и все — ваш новый проект готов:).
Возвращаемся опять к Visual Studio. Перед вами находится дизайнер форм. Щелкните по нему левой кнопкой мыши и выберите в контекстном меню View Code. У вас появится новое окно редактора, в котором расположен код вашего приложения. Давайте приведем его в порядок. Для начала вычищаем все "левые" комментарии. Удалите все строчки, начинающиеся с трех символов наклонной черты (///). Затем отыскиваем строчку #region Windows Form Designer generated code. Видите плюсик справа от нее? Попробуйте пощелкать по нему мышкой.
В ответ на ваши действия большой кусок кода то сворачивается, прячась от глаз, то разворачивается обратно. Это на самом деле довольно удобная функция среды разработки.
Вы можете сами таким образом упорядочивать логические блоки вашего кода. Для этого достаточно ограничить нужный вам кусок кода парой тэгов #region и #endregion. После тэга #region через пробел можно вписать произвольную метку, поясняющую, что именно делает этот блок кода. Как я и говорил раньше, эта функция довольно удобна, но сейчас, пока мы учимся, она нам только мешает. Удалите все строчки, начинающиеся с "#region" и "#endregion", из текста нашей первой учебной программы.
Вычистили все лишнее? Тогда можно сказать, что главная форма нашего будущего приложения готова. Честно говоря, я придумываю этот проект "на ходу", по мере написания текста статьи, и поэтому пока слабо представляю, как именно будет выглядеть визуально наше будущее приложение. Для тех, у кого нет Visual Studio, приведу получившийся код главной формы, а сбоку от символов двойной черты приведу свои пояснения.

// Пример 1
//==начало файла Form1.cs ==
// Подключаем пространства имен (аналог модулей из Delphi)

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;

// Объявляем свое пространство имен (модуль) называющееся Sample1.
namespace Sample1
{

//Объявляем новый класс Form1 (потомок класса Form)

public class Form1 : Form
{

//код, необходимый для корректной работы дизайнера Visual Studio
private Container components = null;

//"Конструктор" нашего объекта Form1. Вызывается во время создания нового экземпляра Form1
public Form1()
{

//Вызов метода, настраивающего нашу форму.
InitializeComponent();

}

//метод, предназначенный для корректного освобождения ресурсов в нашем приложении. Опять-таки, используется только дизайнером Visual Studio.
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null) {components.Dispose();}
}
base.Dispose( disposing );
}

/// Метод, настраивающий форму перед запуском
private void InitializeComponent()
{
//Вы будете смеяться, но следующая строчка опять-таки нужна только дизайнеру форм Visual Studio.
this.components = new System.ComponentModel.Container();

//говорим, что размер нашей формы должен быть 300 на 300 точек.
this.Size = new System. Drawing.Size(300,300);

//Текст, выводимый в заголовке окна, должен быть Form1.
this.Text = "Form1";
}

//Главный метод всей _программы_. Его исполнением начинается главный поток вычисления. В нем же он и заканчивается...
static void Main()
{

//Приложение.Запусти (новый экземпляр формы Form1)
Application.Run(new Form1());
}

}

}
//== конец файла Form1.cs ==

Набранный вами код — это фактически полноценное Net-приложение. Вы можете запустить его на выполнение, выбрав в меню Visual Studio пункт Debug-> Start. Если вы работаете без Visual Studio, наберите вышеприведенный код в вашем любимом текстовом редакторе, сохраните его под именем Form1.cs и выполните файл compile.bat. Результатом его деятельности будет приложение Sample1.exe, которое вы можете и запустить для того, чтобы убедиться, что все работает.
Первое, на что вы наверняка обратили внимание, это обилие кода, предназначенного исключительно для дизайнера Visual Studio. Если вы не планируете им пользоваться, можете вообще не набивать эти строчки текста. Не только компилятор C#, но и сама Visual Studio совершенно нормально обходится и без них. Давайте это мое заявление сразу и проверим, не откладывая это дело в долгий ящик. Приведите вышеуказанный код к вот такому виду.

// Пример 2.
//==начало файла Form1.cs ==

using System;
using System.Windows.Forms;

namespace Sample1
{
public class Form1 : Form {
public Form1(){}
static void Main(){
Application.Run(new Form1());
}
}
}
//===конец файла Form1.cs===

Теперь давайте скомпилируем и запустим наше приложение на выполнение и убедимся, что его внешний вид и работа ничуть не изменились. На первый взгляд, и дизайнер форм в Visual Studio по-прежнему работает.
Если вы попробуете бросить с тулбара на форму, скажем, кнопку, среда разработки восстановит метод Initi-alizeComponent(), в котором создается сама кнопка, но в конструктор формы забудет включить его вызов. Поэтому, хотя выглядеть все в дизайнере будет нормально, но работать приложение, тем не менее, будет неправильно.

В своих примерах я постараюсь научить вас обходиться без дизайнера форм. На мой взгляд, если вы будете хорошо понимать, как именно включаются в форму те или иные компоненты и как они создаются, вам будет проще модифицировать "под себя" и оптимизировать код, создаваемый дизайнером. Поэтому давайте возьмем за основу наших примеров именно эту "краткую" форму кода.
Большинство книг, посвященных программированию на C#, дают примеры, рассчитанные на выполнение в консоли. На мой взгляд, такой подход в корне неверен, так как многие из пользователей Windows уже давно забыли консоль как страшный сон. Сейчас я вам приведу общий скелет нашей стартовой формы, имитирующей при своей работе консоль, но созданной с помощью WinForms. В дальнейшем мы с вами будем модифицировать только код метода Process(), оставляя весь остальной код без изменения. Во-первых, так мы наделаем меньше ошибок, а во-вторых, в статье будет меньше лишней писанины. Вот он, код, имитирующий при работе консоль.

//===конец файла Form1.cs===
// Пример 3.
using System;
using System.Windows.Forms;

namespace Sample1
{
public class Form1 : Form
{
ListBox log = new List Box();
void AddLog(string s) {log.Items.Add(s);}
static void Main(){Appli cation. Run(new Form1());}
public Form1(){
log.Dock=DockStyle.Fill;
this.Controls.Add(log);
Process();
}
void Process(){
AddLog("Старт process...");
AddLog("End process.");
}
}
}
//===конец файла Form1.cs===

Приведите свой код к виду, показанному в "Примере 3". Именно этот вариант мы и станем развивать в следующих примерах.
Ну ладно, с формой, отвечающей за ввод-вывод, мы вроде разобрались, давайте теперь займемся подготовкой к реализации "сущностей", входящих в наше приложение, описывающее процесс варки кофе.
Каждую из сущностей мы будем описывать в своем собственном классе и собственном файле.

На самом деле это совершенно необязательно, но с точки зрения правильности стиля программирования следует поступать именно так.
Для того чтобы создать новый класс, пользователям Visual Studio следует выбрать Project-> Add Class. В появившемся мастере укажите, что вы хотите создать "Class", исправьте название файла на название класса, который вы создаете, оставив расширение .cs. Из кода, созданного мастером, опять-таки убираете все лишние комментарии, и заготовка для нашего класса у нас есть.
Те, у кого Visual Studio нет, просто создайте текстовый файл с нужным содержимым. Сохраните его с именем, совпадающим с именем класса, и расширением .cs. Это совпадение имен также необязательно, но так нам в случае чего будет проще обнаружить нужный файл.
Ну вот, на сегодня, пожалуй, и хватит. Потренируйтесь пока в создании новых классов и компиляции приложения. В следующей статье цикла мы с вами напишем свое первое объектно-ориентированное приложение.

Герман Иванов


Компьютерная газета. Статья была опубликована в номере 16 за 2003 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета