Языки объектно-ориентированного программирования

Методы

Методом класса называют функцию или процедуру, которая принадлежит классу или объекту. Отличие функции от процедуры в том, что функция возвращает значение, а процедура нет. В общем виде синтаксис объявления метода выглядит следующим образом:

модификатор(ы) тип_возвращаемого_значения имя_функции(аргументы)

Модификаторы определяют область видимости, принадлежность метода объекту или классу, является ли метод переопределением и т.п. Тип возвращаемого значения – это любой доступный в C# тип. В качестве типа возвращаемого значения не может использоваться ключевое слово var. Если метод не возвращает ничего, то указывается тип void. Метод может содержать ноль или более аргументов, которые также могут иметь специальные модификаторы, указывающие на то является ли аргумент входным или выходным и т.п. Более подробно про все эти аспекты будет рассказано в одном из уроков, посвященных более глубокому изучению ООП в C#. В рамках данного урока, наша задача – это на интуитивном уровне научиться принципам работы с классами в C#.

Работа с модификатором доступа

Если метод объявлен с модификатором public, то его можно использовать вне класса, например метод Printer из DemoClass

public void Printer()
{
    Console.WriteLine($"field: {field}, Property: {Property}");
}

Такой метод может вызываться в любом месте программы у соответствующих объектов:

var d6 = new DemoClass(11) { Property = 12 };
d6.Printer(); // field: 11, Property: 12

Если мы объявим метод с модификатором private или без модификатора (тогда, по умолчанию, будет принят private), то его уже нельзя будет вызвать снаружи класса:

class DemoClass 
{ 
    // ... 
    private void PrivateMethod() 
    { 
        Console.WriteLine($"Secret method"); 
    } 
    // ... 
}

(Код в методе Main):

var d7 = new DemoClass();
d7.PrivateMethod(); // Ошибка компиляции!!!

Но при этом внутри класса его вызвать можно:

class DemoClass
{
    // ... 
    public void PublicMethod() 
    { 
        Console.WriteLine($"Public method");     
        PrivateMethod(); 
    } 
    // ... 
}

Статические методы и методы объекта

Различают статические методы и методы объекта. Статические имеют модификатор static перед именем метода и принадлежат классу. Для вызова таких методов не обязательно создавать экземпляры класса, мы уже пользовались такими методами из класса Console – это методы Write и WriteLine. Для вызова метода объекта, необходимо предварительно создать экземпляр класса, пример – это метод PublicMethod и Priter у класса DemoClass. Добавим статический метод и метод класса в DemoClass

class DemoClass
{
    // ...
    public static void StaticMethod()
    {
        Console.WriteLine("Message from static method");
    }
    public void NoneStaticMethod()
    {
        Console.WriteLine("Message from non static method");
    }
    // ...
}

Вызовем эти методы из класса DemoClass в методе Main

DemoClass.StaticMethod(); // Message from static method
var d8 = new DemoClass();
d8.NoneStaticMethod(); // Message from none static method

Методы принимающие аргументы и возвращающие значения

Как было сказано в начале данного раздела, методы могут принимать данные через аргументы и возвращать значения, продемонстрируем эту возможность на примере:

class DemoClass 
{ 
    // ... 
    public int MulField(int value) 
    { 
        return field * value; 
    } 
    // ... 
}

(Код в Main):

var d8 = new DemoClass(10);
Console.WriteLine($"MulField() result: {d8.MulField(2)}"); // MulField() result: 20

Геттеры и сеттеры

Очень часто все свойства объекта делают приватными,
а для доступа к ним реализуют специальные методы.

Например, у нас есть свойство ->name — сделаем его приватным,
а взамен сделаем метод getName(), который будет возвращать
содержимое этого свойства во внешний мир, и метод setName(новое имя),
который будет менять значение этого свойства.

Аналогично поступим с возрастом и вот, что у нас получится:

Давайте теперь разбираться, зачем это нужно.

Во-первых, таким образом мы можем создавать свойства только для чтения.
Например, возраст можно будет только получить через getAge,
но нельзя будет поменять без setAge.

Во-вторых, таким образом мы защищаемся от несанкционированных изменений
этого свойства — его нельзя будет затереть случайно,
а обязательно нужно будет вызвать метод.

Во-третьих, мы можем проверять, к примеру, новые значения возраста —
а вдруг кто-то пытается установить некорректный возраст — реализуем
проверку возраста в setAge и будем изменять свойство age,
только если возраст корректный.

Такие методы, как мы реализовали, называются геттерами
и сеттерами.

Плюсы и минусы объектно-ориентированных языков программирования

Хотя языки ООП могут быть мощными, они полезны не для каждой ситуации и имеют некоторый багаж, который необходимо учитывать.

Плюсы

1. Возможность повторного использования

Объектно-ориентированный код имеет чрезвычайно модульную структуру. Благодаря полиморфизму и абстракции вы можете создать одну функцию, которую можно будет использовать снова и снова. Вы также можете скопировать информацию и функции, которые уже были написаны с помощью наследования. Это экономит время, снижает сложность, экономит место и упрощает программирование.

2. Параллельная разработка

Существует достаточно наработок для того, чтобы части программы разрабатывались отдельно друг от друга и при этом функционировали по объектно-ориентированным принципам. Это значительно упрощает параллельную разработку для более крупных команд разработчиков.

3. Обслуживание

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

4. Безопасность

Хотя большинство языков имеют некоторую безопасность, объектно-ориентированные языки удобны, поскольку безопасность встроена с инкапсуляцией. Другие методы и классы не могут получить доступ к личным данным по умолчанию, и программы, написанные на языках ООП, более безопасны для этого.

5. Модульность

В объектно-ориентированных языках программирования приложение разбивается на объекты и классы. Это полезно, потому что даёт вашему приложению более модульную структуру. Модульный код легче читать. Поэтому его легче обслуживать.

Минусы

1. Часто грязный

Поскольку объектно-ориентированные языки настолько настраиваемы и масштабируемы, можно легко потерять понимание того, как работает код. Код ООП может работать разными способами. Существует множество методологий программирования в ООП, которые не работают с другими методологиями, неэффективны или сложны в использовании.

2. Требуется больше планирования

Поскольку эти языки настолько модульны и масштабируемы, отказ от чёткой конструкции заранее — это рецепт катастрофы. Создание эффективной программы требует твёрдого плана в большей степени, чем при использовании других парадигм программирования.

3. Непрозрачность

Это как плюс, так и минус. Объекты и функции могут работать независимо. Они могут получать информацию и (обычно) возвращать надёжные результаты. В результате они могут оказаться чёрными ящиками, а это означает, что то, что они делают, не всегда очевидно. Хотя программист, вероятно, создал этот объект и знает, что он делает, языки ООП просто не так прозрачны, как другие языки.

4. Производительность

Объектно-ориентированные языки часто страдают от снижения производительности. Программы, написанные на языках ООП, часто больше и требуют больше вычислительных усилий для запуска, чем функциональные языки

Однако это не всегда так или важно. C ++ — язык ООП, но это один из самых быстрых доступных языков

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

Абстракция

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

Эффективным средством применения абстракции служат иерархические клас­сификации. Это позволяет упрощать семантику сложных систем, разбивая их на более управляемые части. Внешне автомобиль выглядит единым объектом. Но стоит заглянуть внутрь, как становится ясно, что он состоит из нескольких под­систем: рулевого управления, тормозов, аудиосистемы, привязных ремней, обо­гревателя, навигатора и т.п. Каждая из этих подсистем, в свою очередь, собрана из более специализированных узлов. Например, аудиосистема состоит из радиопри­емника, проигрывателя компакт-дисков и/или аудиокассет. Суть всего сказанного состоит в том, что структуру автомобиля (или любой другой сложной системы) можно описать с помощью иерархических абстракций.

Иерархические абстракции сложных систем можно применять и к компьютер­ным программам. Благодаря абстракции данные традиционной, ориентирован­ной на процессы, программы можно преобразовать в составляющие ее объекты, а последовательность этапов процесса — в совокупность сообщений, передавае­мых между этими объектами. Таким образом, каждый из этих объектов описывает свое особое поведение. Эти объекты можно считать конкретными сущностями, реагирующими на сообщения, предписывающие им вътолнитьконкретное действие. В этом, собственно, и состоит вся суть ООП.

Принципы ООП лежат как в основе языка Java, так и восприятия мира человеком

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

Например, при наличии тщатель­но определенных объектов и ясных, надежных интерфейсов с этими объектам можно безбоязненно и без особого труда извлекать или заменять части старой системы.

Понятия объектно-ориентированного программирования

Попробую, используя перечисленные утверждения, выполнить анализ основных понятий объектно-ориентированного программирования. Этот анализ обходит понятие абстрагирование, так как оно уже было описано ранее в формализации способа построения универсального алгоритма.

Класс, Объект

Данные понятия ООП закрепляют целесообразность использования специального вида компонента, описываемого совокупностью некоторых внутренних данных и методов работы с этими данными. Все утверждения группы и транслируются в ООП, для которого термин компонент заменяется понятием класс.

При этом, на первый взгляд, отношения класса и объекта исчерпываются группой утверждений , в которой база заменяется понятием класс, а реализация — понятием объект. Причем реализация получается динамическая, то есть изменяемая в процессе исполнения программы.

Инкапсуляция

Понятие «инкапсуляция» можно рассмотреть с двух «сторон».

Первая сторона понятия «инкапсуляция» — это обособленность компонента от других участков кода. Это свойство позволяет программисту для внесения изменений в компонент выполнить операции в участках кода, которые расположены «близко». То есть минимизировать затраты времени программиста, исключая из работы поиск и анализ разрозненных взаимодействующих элементов программы. Эта сторона задается свойствами компонента, следующими из его определения.

Вторая сторона понятия «инкапсуляция» — это сокрытие внутренней реализации компонента. Это сокрытие возможно с использованием понятий база и реализация, описанных в группе утверждений . Для этого публичные методы класса отождествляются с базой, а приватные и защищенные методы класса — с реализацией. В местах использования используются ограничения, формируемые базой, и потому появляется возможность производить изменения в реализации, не касающиеся базовых ограничений. И эти изменения реализации не нужно проверять в местах использования базы , что обеспечивает минимизацию трудозатрат программиста.

Наследование

Понятия «наследование» продолжает закреплять важность использования связки база + реализация. Для этого в группе утверждений необходимо методы родительского класса отождествить с базой, а методы класса-наследника отождествить с реализацией

В своей реализации понятие «наследование» позволяет использовать утверждение , то есть использовать дополнение кода вместо его изменения и дублирования. При этом необходимо исключить дублирование базового алгоритма. Однако, у подхода, использующего наследование для специализации универсального алгоритма, есть существенный минус. Этот недостаток — наличие двух сильно-связных компонентов, которые тяжело изменять независимо. Эти связи-зависимости порождаются отношением родитель-наследник.

Существует множество альтернативных способов использовать связку база + реализация. Приведу далее примеры таких способов.

База Реализация Область применения
Публичные методы класса Приватные методы класса Инкапсуляция
Защищенные методы родительского класса Методы класса-наследника Наследование
Интерфейс динамической библиотеки Функционал динамической библиотеки Компонент=динамическая библиотека
Шаблонные (обобщенные) методы и классы (template, generic) Инстанцирование шаблона с указываемыми аргументами Обобщенное программирование
Универсальные методы, принимающие делегаты Специализация методов указанием конкретных процедур обработки Процедуры сортировки или формирования дерева, с указанием метода оценки порядка элементов
Классы, предусматривающие взаимодействие с шаблоном «Посетитель» Формирование «Посетителя» с требуемым функционалом Шаблон проектирования «Посетитель»
Панель управления АЭС Совокупность автоматики и оборудования АЭС Сокрытие сложности системы от оператора АЭС

Полиморфизм

По моему мнению, понятие «полиморфизм» — это вторая сторона при взгляде на процедуру создания универсального алгоритма. Первая сторона ( абстрагирование) — это взгляд с точки зрения способов создания универсального алгоритма. В то же время при взгляде на универсальный алгоритм с точки зрения пользователя, получаем запись понятия полиморфизм. То есть полиморфизм это полезная способность функции (компонента) обрабатывать данные разных типов. Добавление этого понятия в ООП закрепляет полезность использования универсального алгоритма в разработке программного проекта.

Наследование[]

Наследование — один из четырёх важнейших механизмов объектно-ориентированного программирования (наряду с инкапсуляцией, полиморфизмом и абстракцией), позволяющий описать новый класс на основе уже существующего (родительского), при этом свойства и функциональность родительского класса заимствуются новым классом.

Другими словами, класс-наследник реализует спецификацию уже существующего класса (базовый класс). Это позволяет обращаться с объектами класса-наследника точно так же, как с объектами базового класса.

Простое наследование:

Класс, от которого произошло наследование, называется базовым или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).

В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».

Множественное наследование

При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.

Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

Попытка решения проблемы наличия одинаковых имен методов в предках была предпринята в языке Эйфель, в котором при описании нового класса необходимо явно указывать импортируемые члены каждого из наследуемых классов и их именование в дочернем классе.

Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

Парадигмы программирования

В этой связи языки программирования часто разделяются по парадигме программирования. Парадигма программирования — это один из способов взглянуть на данные и получить к ним доступ. Две основные парадигмы являются объектно-ориентированным и функциональным, хотя существует многие другие.

Объектно-ориентированное программирование строится вокруг объектов, которые представляют собой структуры данных, содержащие как данные (свойства или атрибуты), так и код (процедуры или методы). Объекты могут изменять себя с помощью «this» или «self». В большинстве языков ООП почти все является объектом, который может иметь как значения, так и исполняемый код. Каждый объект уникален, и хотя он может быть копией другого объекта, его переменные могут отличаться от переменных любого другого объекта.

Объекты в объектно-ориентированном программном обеспечении можно рассматривать как реальные объекты. Думайте об объекте, как о часах. У этих часов есть свойства. Он металлический, чёрный, имеет удельный вес. Но этот объект тоже что-то делает. Он отображает время, а также может влиять на себя, вращая шестерёнки, чтобы изменить положение на руке.

Ещё одна особенность объектов заключается в том, что нам не всегда нужно знать, как работают часы, чтобы заставить их работать. Предполагая, что часы хорошо построены, они будут надёжно определять время, и нам не придётся вмешиваться в их внутреннюю работу.

В объектно-ориентированных языках есть объекты, похожие на объекты реального мира. У них могут быть свойства и функции. Они также склонны следовать определённому набору принципов.

Поля

Поле представляет собой переменную любого типа, объявленную внутри класса. Через модификатор доступа можно управлять уровнем доступа к полю (так же как для методов), через ключевое слово static можно определять принадлежность поля объекту либо классу:

class DemoClass 
{ 
    // ... 
    int field = 0; 
    public int publicField = 0; 
    public static int publicStaticField = 0; 
    // ...
}

(Код в Main):

var d9 = new DemoClass();
// Доступ к private полям запрещен
// Console.WriteLine($"Get private field: {d9.field}"); // Compile ERROR           
// Доступ к полю объекта
d9.publicField = 123;
Console.WriteLine($"Get public field: {d9.publicField}"); // Get public field: 123
// Доступ к статическому полю класса
DemoClass.publicStaticField = 456;
Console.WriteLine($"Get public static field: {DemoClass.publicStaticField}"); // Get public static field: 456

Работать с открытыми полями напрямую (поля, которые имеют модификатор public) является плохой практикой. Если необходимо читать и изменять значение какого-либо поля, то лучше это делать через getter’ы и setter’ы – специальные методы, которые выполняют эту работу.

Создадим для класса Building методы для доступа и модификации значения поля height

class Building 
{ 
    float height;

    public float GetHeight() => height;
    public float SetHeight(float height) => this.height = height; 
}

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

var b1 = new Building();
b1.SetHeight(12);
Console.WriteLine($"Height of building: {b1.GetHeight()}");

Создание специальных методов для работы с полями – возможный вариант, но в C# принят подход работы через свойства. Им посвящен следующий раздел.

Классы

Класс в языке C# объявляется с помощью ключевого слова class перед ним могут стоять несколько модификаторов, после располагается имя класса. Если предполагается, что класс является наследником другого класса или реализует один или несколько интерфейсов, то они отделяются двоеточием от имени класса и перечисляются через запятую.

class Building 
{
}

Внутри себя, класс может содержать методы, поля и свойства. Методы похожи на функции из языков группы структурного программирования. Фактически они определяют то, как можно работать с данным классом или объектами класса. Поля – это переменные, связанные с данным классом, а свойства – это конструкции специального вида, которые упрощают работу с полями (в первом приближении такого понимания будет достаточно).

class DemoClass
{
    // Поле класса
    int field = 0;
      
    // Свойство класса
    public int Property {get;set;}
      
    // Метод класса
    public void Method()
    {
        Console.WriteLine("Method");
    }
}

Далее, мы подробно остановимся на каждом из перечисленных составляющих класса.

Объект класса создается также как переменная любого, уже известного вам по предыдущим урокам, типа данных. Напомним, что класс является типом данных, который мы можем самостоятельно объявить. Все встроенные типы являются определенными классами, например, переменная типа int – это объект класса System.Int32.

Создадим объект класса DemoClass

// Создание объекта класса DemoClass
DemoClass demo = new DemoClass();
// Вызов метода Method объекта demo
demo.Method();

С методами встроенных типов мы уже встречались ранее, например, метод поиска элемента в строке, или преобразование строки в число и т.п.

Что такое ООП?

Я подойду к вопросу с редукционистских позиций. Есть много правильных определений ООП которые покрывают множество концепций, принципов, техник, паттернов и философий. Я намерен проигнорировать их и сосредоточиться на самой соли. Редукционизм тут нужен из-за того, что всё это богатство возможностей, окружающее ООП на самом деле не является чем-то специфичным для ООП; это просто часть богатства возможностей встречающихся в разработке программного обеспечения в целом. Тут я сосредоточусь на части ООП, которая является определяющей и неустранимой.

Посмотрите на два выражения:

1: f(o); 2: o.f();

В чём разница?

Никакой семантической разницы явно нет. Вся разница целиком и полностью в синтаксисе. Но одно выглядит процедурным, а другое объектно ориентированным. Это потому что мы привыкли к тому, что для выражения 2. неявно подразумевается особая семантика поведения, которой нет у выражения 1. Эта особая семантика поведения — полиморфизм.

Когда мы видим выражение 1. мы видим функцию f, которая вызывается в которую передаётся объект o. При этом подразумевается, что есть только одна функция с именем f, и не факт, что она является членом стандартной когорты функций, окружающих o.

С другой стороны, когда мы видим выражение 2. мы видим объект с именем o которому посылают сообщение с именем f. Мы ожидаем, что могут быть другие виды объектов, котоые принимают сообщение f и поэтому мы не знаем, какого конкретно поведения ожидать от f после вызова. Поведение зависит от типа o. то есть f — полиморфен.

Вот этот факт, что мы ожидаем от методов полиморфного поведения — суть объектно ориентированного программирования. Это редукционистское определение и это свойство неустранимо из ООП. ООП без полиморфизма это не ООП. Все другие свойства ООП, такие как инкапсуляция данных и методы привязанные к этим данным и даже наследование имеют больше отношения к выражению 1. чем к выражению 2.

Программисты, использующие Си и Паскаль (и до некоторой степени даже Фортран и Кобол) всегда создавали системы инкапсулированных функций и структур. Чтобы создать такие структуры даже не нужен объектно ориентированный язык программирования. Инкапсуляция и даже простое наследование в таких языках очевидны и естественны. (В Си и Паскале более естественно, чем в других)

Поэтому то, что действительно отличает ООП программы от не ООП программ это полиморфизм.

Возможно вы захотите возразить, что полифорфизм можно сделать просто используя внутри f switch или длинные цепочки if/else. Это правда, поэтому мне нужно задать для ООП ещё одно ограничение.

Использование полиморфизма не должно создавать зависимости вызывающего от вызываемого.

Чтобы это объяснить, давайте ещё раз посмотрим на выражения. Выражение 1: f(o), похоже зависит от функции f на уровне исходного кода. Мы делаем такой вывод потому что мы также предполагаем, что f только одна и что поэтому вызывающий должен знать о вызываемом.

Однако, когда мы смотрим на Выражение 2. o.f() мы предполагаем что-то другое. Мы знаем, что может быть много реализаций f и мы не знаем какая из этих функций f будет вызвана на самом деле. Следовательно исходный код, содержащий выражение 2 не зависит от вызываемой функции на уровне исходного кода.

Если конкретнее, то это означает, что модули (файлы с исходным кодом), которые содержат полиморфные вызовы функций не должны ссылаться на модули (файлы с исходным кодом), которые содержат реализацию этих функций. Не может быть никаких include или use или require или каких-то других ключевых слов, которые создают зависимость одних файлов с исходным кодом от других.

Итак, наше редукционистское определение ООП это:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector