Направленное ориентированное программирование (OOPS или OOPS для объектно-ориентированной системы программирования) является сегодня наиболее широко используемой парадигмой программирования. Хотя большинство популярных языков программирования являются мультипарадигмальными, поддержка объектно-ориентированного программирования является фундаментальной для больших проектов. Конечно, ООП имеет свою долю критиков. В настоящее время функциональное программирование кажется новым модным подходом. Тем не менее, большинство критиков в основном из-за неправильного использования ООП.
Это означает, что если вы учитесь становиться лучшим программистом, важно иметь представление об основных понятиях объектно-ориентированного программирования и о том, как они работают. Может быть, вы опытный программист, но вы начали прямо с практики, без каких-либо теоретических знаний. Или вы просто забыли обновить свои знания во время работы. Вы можете быть удивлены тем, чего на самом деле не знаете. Видимо, это может случиться с лучшими из нас .
Поэтому мы постараемся сохранить правильное сочетание теории и практики, приведя множество примеров. Наши примеры будут основаны на представлении командного вида спорта: наш домен будет посвящен игрокам, тренерам и другим сотрудникам. Как вы это представляете? Мы собираемся ответить на это.
Содержание
Класс
Каждый игрок — это отдельный человек, но у всех них есть что-то общее: они могут выполнять одни и те же действия, такие как бег или пас, и у них есть определенные функции, такие как число и позиция. То же самое можно сказать и о тренерах и остальном персонале. У каждого из них будут разные комбинации, но все они следуют одной и той же модели.
Класс — это модель, план, шаблон, описывающий некоторые функции.
Точнее, класс представляет данные, обычно с помощью переменных, называемых полями , и поведения, представленные функциями, обычно называемыми методами . Например, класс Player
может иметь поле с именем Role
для представления своей роли или положения на фактическом поле игры, Name
, для представления своего имени. В качестве поведения он может иметь метод Pass(Player)
, который заставит его передать мяч другому игроку.
Давайте посмотрим пример в псевдокоде.
Класс
01
02
03
04
05
06
07
08
09
10
|
class Player { Text Name Text Role Pass (Player teamMate) { // body of the method } } |
объект
Если класс в модели, каковы фактические игроки? Они называются объектами или экземплярами класса. В предыдущем примере аргумент teamMate был объектом. Чтобы создать объект из класса, вы создаете экземпляр или создаете объект.
Например, объект класса Player
John имеет Name
«Johnny John» и Role
«Attacker».
1
2
3
|
Player John = new Player John.Name = "Johnny John" John.Role = "Attacker" |
Черный ящик
Черный ящик — это то, что вы можете наблюдать за вводом и выводом, но вы игнорируете, как он работает: вы не можете заглянуть внутрь. Это может быть хорошо, потому что вы не зависите от того, что находится внутри коробки. И вам все равно, если однажды кто-то изменит то, что находится внутри коробки, если все еще будет казаться, что он ведет себя так же, как снаружи. Это принцип, который применяется в ООП, и это хорошо.
Проще говоря: если вы просто знаете, что что-то должно делать, но не знаете, как это происходит, вы не можете испортить это.
Идея состоит в том, чтобы делегировать все, что необходимо, чтобы сделать что-то важное для определенного раздела кода. Так что вы можете изменить его, независимо от других, без риска что-то сломать.
Например, представьте, что тренер создал определенную стратегию: ему не нужно объяснять игрокам, как проходить или как бегать. Надо просто сказать им, что им делать. Сами игроки должны знать, как на самом деле делать эти вещи. Мы хотим достичь той же организации в нашем программировании.
Мы можем достичь этого с помощью абстракции и инкапсуляции .
Давайте начнем с общего псевдокода.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
class Coach { TellHimToRun(Player dude) { dude.Run() } } class Player { // the class BodyPart is not shown BodyPart Legs Run() { if Legs.IsOk() // do the running else // do something hilariously bad } } |
абстракция
Абстракция относится к сокрытию деталей реализации извне класса.
Как пример, объект OldMan класса Coach
вызывает метод Run()
Джона, объект класса Player
. Это не то, что Джон должен сделать, чтобы действительно бежать. Нужно просто знать, что у объекта класса Player
есть метод Run()
.
Инкапсуляция
Инкапсуляция включает в себя два понятия: ограничение доступа к некоторым полям и методам класса из внешнего мира и связывание воедино связанных данных и методов.
Например, чтобы имитировать возможность запуска, у класса Player
есть метод Run()
, но у него также есть поле с именем Legs
. Это поле отображает состояние ног игрока: насколько они устали и каково их здоровье, то есть травмированы ли они. Внешний мир не должен знать, что у Player
есть Legs
и как они работают.
Так что класс Player
скрывает поле Legs
от внешнего мира. Поскольку для выполнения Run()
ему просто нужно работать на Legs
, тем самым он гарантирует, что отдельный объект может быть полностью автономным от внешних помех. Это полезно, если позже вы захотите добавить к симуляции эффекты разных ботинок. Вам просто нужно изменить класс Player
, и больше ничего.
наследование
Чтобы решить проблему, вы обычно создаете классы, которые так или иначе связаны между собой. Они имеют некоторые характеристики или даже некоторые виды поведения. Поскольку вы хотите избежать повторений и, следовательно, ошибок, вы хотите собрать все эти общие функции в общий класс. Обычно этот класс называется родительским, супер или базовым классом. Как только вы создали этот класс, другие классы могут объявить, что он похож на него или наследуется от него. Это называется наследством.
Конечным результатом является то, что каждый из классов, которые наследуются от родительского класса, может также иметь методы и поля родительского класса, в дополнение к своим собственным.
Например, вы заметили, что классы Player
, Coach
и Staff
имеют имя и зарплату, поэтому вы создаете родительский класс с именем Person
и заставляете их наследовать от него. Обратите внимание, что вы просто продолжаете создавать только объект класса Player
, Coach
и т. Д. Вам не нужно явно создавать объект класса Person
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class Person { Integer Salary Text Name } class Coach : parent Person { AskForARaise() { if Salary < 1000 // request more money } } |
В некоторых языках вы можете явно запретить создание класса Person
, пометив его как абстрактный класс . В таких случаях класс, который вы можете создать, называется конкретным классом .
Большинство объектно-ориентированных языков поддерживают наследование, некоторые также поддерживают множественное наследование : класс может наследоваться от нескольких классов. Это не всегда возможно, потому что это создает проблемы и добавляет сложности. Типичная проблема — решить, что делать, когда два разных родительских класса имеют метод с одинаковой сигнатурой.
В общих чертах наследование определяет отношение между двумя классами (-a-type-of) -a . В нашем примере Player
— это (-a-type-of) -a Person
.
Интерфейс
Интерфейс, также известный как протокол, является альтернативой наследования для двух не связанных между собой классов для связи друг с другом. Интерфейс определяет методы и (часто, но не всегда) значения. Каждый класс, который реализует интерфейс, должен предоставить конкретный метод для каждого метода интерфейса.
Например, вы хотите смоделировать выброс или увольнение. Поскольку с поля могут быть выброшены только игроки и тренеры, вы не можете сделать это методом родительского класса, представляющего людей. Таким образом, вы создаете интерфейс Ejectable
с методом Ejection()
и заставляете Player
и Coach
реализовывать его.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
interface Ejectable { Ejection() } class Player : implement Ejectable { Ejection() { // storm out of the field screaming } } class Coach : implement Ejectable { Ejection() { // take your cellphone to talk with your assistant } } |
Не существует стандартного способа описания отношений, которые устанавливаются интерфейсом, но вы можете думать, что они behave-as
. В нашем примере Player
ведет себя как Ejectable
.
Ассоциация, Агрегация, Состав
Наследование и интерфейс применяются к классам , но существуют возможные способы связать два или более разных объекта. Эти способы можно представить в порядке расшатанности отношений: ассоциация, агрегация и состав.
ассоциация
Ассоциация просто описывает любой вид рабочих отношений. Два объекта являются экземплярами совершенно не связанных классов, и ни один из объектов не контролирует жизненный цикл другого. Они просто сотрудничают для достижения своих собственных целей.
Представьте, что вы хотите добавить влияние аудитории на игроков, в реальной жизни аудитория состоит из людей, но в нашем симуляции они не дети класса Person
. Вы просто хотите сделать это, если объект HomePeople класса Audience
приветствует, то Джон играет лучше. Таким образом, HomePeople может повлиять на поведение Джона, но ни HomePeople, ни Джон не могут контролировать жизненный цикл другого.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class Audience { Boolean Cheering Cheer() { Cheering = true } StopCheer() { Cheering = false } } Audience HomePeople = new HomePeople class Player { ListenToThePeople() { if HomePeople.Cheering = true // improve ability } } |
агрегирование
Агрегация описывает отношения, в которых один объект принадлежит другому объекту, но они все еще потенциально независимы. Первый объект не контролирует жизненный цикл второго.
В командном спорте все объекты класса Player
принадлежат объекту Team
, но они не умирают только потому, что уволены. Они могут быть безработными или сменить Team
.
Этот тип отношений обычно описывается как has-a ( или is-part-of) или наоборот. В нашем примере объект Winners of Team
имеет — John of Player
или, наоборот, John принадлежит — Winners .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class Team { Player[ 50 ] TeamMembers Fire(Player dude) { TeamMembers.Remove(dude) } Hire(Player dude) { TeamMembers.Add(dude) } } Team Winners = new Team Team Mediocre = new Team Player John = new Player Winners.Hire(John) // time passes Winners.Fire(John) Mediocre.Hire(John) // |
Состав
Композиция описывает отношения, в которых один объект полностью контролирует другой объект, который не имеет независимого жизненного цикла.
Представьте, что мы хотим добавить стадионы или арены в нашу симуляцию. Мы решаем, что объект Arena
не может существовать вне Team
, он принадлежит Team
которая решает их судьбу. Конечно, в реальной жизни арена волшебным образом не исчезает, как только команда решает уволить ее. Но поскольку мы хотим симулировать только командные виды спорта для нашей цели, он выходит из игры, как только перестает принадлежать команде.
Композиции описываются так же, как агрегаты, поэтому обратите внимание, чтобы не путать их.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
class Arena { Text Name Integer Capacity } class Team { Arena HouseOfTheTeam EvaluateArena() { // if arena is too small for our league HouseOfTheTeam.Destroy() // create a new Arena HouseOfTheTeam = new Arena } } |
Полиморфизм
Полиморфизм в контексте ООП означает, что вы можете вызывать одну и ту же операцию над объектами разных классов, и все они будут выполнять ее по-своему. Это отличается, и не следует путать с концепцией программирования, которая не зависит от перегрузки ООП: функция (метод). Перегрузка применяется к функциям и позволяет определять функции с одинаковыми именами, которые работают с разными и не связанными типами. Например, вы можете сделать два метода add()
: один для добавления целых чисел и другой для добавления действительных чисел.
Полиморфизм в классе обычно означает, что метод может работать с различными объектами связанных классов. Он может вести себя по-разному на разных объектах, но это не обязательно. На самом базовом уровне можно использовать объект дочернего класса , наследуемый от родительского класса , когда вы можете использовать объект родительского класса. Например, вы можете создать функцию Interview(Person)
которая имитирует интервью с Player
, Coach
или Staff
. Неважно, фактический объект. Очевидно, чтобы это работало, Interview
может работать только с полями, присутствующими в классе Person
.
На практике это означает, что объект дочернего класса также является объектом родительского класса.
В некоторых случаях дочерний класс может переопределять метод родительского класса с тем же именем, это называется переопределением . В этой ситуации всякий раз, когда этот метод вызывается, фактически выполняемый метод является дочерним классом. Это особенно полезно, когда вам нужно, чтобы все дочерние классы имели определенное поведение, но нет способа определить универсальный. Например, вы хотите, чтобы все объекты класса Person
на пенсию, но Player
должен уйти в отставку после определенного возраста, а Coach
или Staff
— нет.
Делегация и открытая рекурсия
В объектно-ориентированном программировании делегирование относится к оценке члена одного объекта (получателя) в контексте другого, исходного объекта (отправителя) — из Википедии.
Эта концепция широко используется в конкретном стиле объектно-ориентированного программирования, который называется программированием на основе прототипов. Одним из распространенных языков, который использует его, является JavaScript. Основная идея состоит в том, чтобы не иметь классов, а только объекты. Таким образом, вы не создаете экземпляры объектов из класса, но клонируете их из общего и затем изменяете его прототип в соответствии с вашими потребностями.
Несмотря на то, что он является ядром самого известного языка программирования, он малоизвестен. Ведь даже JavaScript он в основном используется в обычном стиле объектно-ориентированного программирования. Хотя это может показаться загадочным знанием, важно знать это, потому что оно широко используется со специальной переменной или ключевым словом, называемым this или self .
Большинство объектно-ориентированных языков программирования поддерживают его, и это позволяет ссылаться на конкретный объект (не на класс), который будет создан, внутри метода, определенного в классе, или в любом дочернем классе . Тот факт, что когда код будет выполняться, this
будет ссылаться на конкретный объект, это то, что позволяет открывать рекурсию . Это означает, что базовый класс может определять метод, который использует this
для ссылки на один из его методов, который фактический объект будет использовать для ссылки на дочерний метод с той же сигнатурой.
Это звучит сложно, но это не так. Представьте себе следующий псевдокод.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
class Person { Integer Age IsOld() { if this .Age > 60 return true else return false } DoesHaveToRetire() { // delegation will happen here at runtime if this .IsOld() return true else return false } } class Player : parent Person { function IsOld() { if this .Age > 34 return true else return false } } Player John John.Age = 35 // this is where open recursion happens // the answer is true John.DoesHaveToRetire() |
В последней строке находится рекурсия open, потому что метод DoesHaveToRetire
— это метод, определенный в родительском классе, который использует this.IsOld()
(в строке 16). Но метод IsOld
который фактически вызывается во время выполнения, — это метод, определенный в дочернем классе Player
.
Это также делегирование, поскольку в строке 16 this
оценивается в контексте объекта John, оцениваемого как объект класса Player
, а не как оригинал this John , как объект класса Person
. Потому что помните, что Джон является одновременно объектом класса Player
и его родительским классом Person
.
Симптомы плохого дизайна
До сих пор мы говорили об основах. Мы считаем, что некоторым людям нужно больше, чтобы плодотворно применять эти знания. Во-первых, нам нужно посмотреть на признаки плохого дизайна, чтобы вы могли обнаружить их для своего кода.
жесткость
Программное обеспечение трудно изменить, даже для мелочей. Каждая модификация требует каскадных изменений, для применения которых требуются недели. Сами разработчики понятия не имеют, что произойдет и что нужно будет изменить, когда им нужно будет выполнить X или Y. Это приводит к нежеланию и страху перед изменениями как у разработчиков, так и у руководства. И это медленно делает код очень сложным в обслуживании.
хрупкость
Программное обеспечение неожиданно ломается для каждого изменения. Это проблема жесткости, но она другая, потому что нет последовательности изменений, которая с каждым часом становится все длиннее. Кажется, все работает, но когда вы думаете, что готовы отправить код, тест или, что еще хуже, заказчик, говорит вам, что что-то другое больше не работает. Новая вещь работает, но другая сломана. Каждое исправление — это две новые проблемы. Это приводит к существенному страху у разработчика, который чувствует, что он потерял контроль над программным обеспечением.
Unportability
Каждый модуль работает, но только в осторожной ситуации, в которой он был размещен. Вы не можете повторно использовать код в другом проекте, потому что это слишком много мелочей, которые вам придется изменить. Программа, кажется, работает с помощью темной магии. Там нет дизайна, только хаки. Каждый раз, когда вы изменяете что-то, вы знаете, что делать, но это всегда ужасная вещь, которая заставляет вас бояться, что она снова вас укусит. И это будет. Помимо позора, который вы действительно должны чувствовать, он затрудняет повторное использование кода. Вместо этого вы воссоздаете немного другой код, который почти делает то же самое.
Принципы хорошего дизайна
Чтобы понять, как выглядит проблема, этого недостаточно. Нам также нужно знать практические принципы проектирования, чтобы в первую очередь избежать создания плохого дизайна. Эти общеизвестные принципы являются плодом многолетнего опыта и известны под их аббревиатурой: SOLID.
Единственная ответственность
Никогда не должно быть более одной причины для изменения класса 1
Обычно причина изменения изменяется в «ответственности», отсюда и название принципа, но это оригинальная формулировка. В этом контексте ответственность или причина изменения зависит от конкретного проекта и его требований. Это, очевидно, не означает, что у класса должен быть только один метод. Это означает, что когда вы описываете, что он делает, вы говорите, что он делает только одну вещь. Если вы нарушаете этот принцип, разные обязанности становятся связанными, и вам может потребоваться сменить один или несколько классов по многим разным причинам.
Давайте вернемся к нашему примеру. Вам нужно вести счет игры. Вы можете испытать желание использовать класс Player
, в конце концов, игроки делают подсчет очков. Но если вы сделаете это, каждый раз, когда вам нужно знать счет, вам также нужно будет взаимодействовать с Игроком. И что бы вы сделали, если вам нужно обнулить точку? Сохраняйте одну работу для каждого класса.
Открыто закрыто
Модуль должен быть открыт для расширения, но закрыт для модификации 2
В этом контексте модуль означает класс или группу классов, которые заботятся об одной цели программного обеспечения. Этот принцип означает, что вы должны иметь возможность добавлять поддержку для новых элементов без необходимости изменения кода самого модуля. Например, вы должны иметь возможность добавлять новый тип игрока (например, хранителя) без изменения класса Player
.
Это позволяет разработчику поддерживать новые вещи, которые выполняют те же функции, что и те, которые у вас уже есть, без необходимости делать «особый случай» для этого.
Лисковская замена
Подклассы должны использоваться в качестве базовых классов. 2
Обратите внимание, что это не оригинальная формулировка, потому что она слишком математична. Это настолько важный принцип, что он был включен в дизайн объектно-ориентированного языка. Но сам язык гарантирует только часть принципа, формальную часть. Технически вы всегда можете использовать объект подкласса, как если бы он был объектом базового класса, но для нас важно также практическое использование.
Это означает, что вы не должны существенно изменять поведение подкласса, даже когда вы переопределяете метод. Например, если вы играете в гоночную машину, вы не можете создать подкласс для машины, которая движется под водой . Если вы сделаете это, метод Move()
будет вести себя не так, как базовый класс. А через несколько месяцев будет странная ошибка случайных автомобилей, берущих за собой ручей в качестве шоссе.
Разделение интерфейса
Многие клиентские интерфейсы лучше, чем один универсальный интерфейс 2
В этом контексте «специфический для клиента» означает специфический для каждого типа клиента, а не для каждого клиентского класса. Этот принцип говорит о том, что вы не должны реализовывать один общий интерфейс для клиентов, которые действительно делают совсем другие вещи. Это потому, что это связывает каждый тип клиента друг с другом. Если вы модифицируете один тип клиента, вам нужно изменить общий интерфейс и, возможно, вам также придется изменить другие клиенты. Вы можете распознать нарушение этого принципа, если у вас есть несколько методов интерфейса, специфичных для одного клиента. Это может быть как в том смысле, что они делают обычные вещи по-другому, так и в том, что они делают вещи, которые нужны только одному клиенту.
При практическом использовании это, вероятно, сложнее уважать. Тем более, что в начале легко не заметить, какие на самом деле разные требования. Например, представьте, что вам приходится иметь дело с соединениями: кабельным соединением, мобильным соединением и т. Д. Они все одинаковые, не так ли?
Ну, теоретически они ведут себя одинаково, но на практике они могут отличаться. Мобильные соединения, как правило, стоят дороже и имеют строгие ограничения на размер ежемесячно передаваемых данных. Таким образом, мегабайт для мобильной связи дороже, чем для кабельной. Как следствие, вы можете реже проверять данные или использовать SendBasicData()
вместо SendData()
… Если у вас уже нет опыта работы в конкретной области, вы можете заставить себя следовать этому принципу.
Инверсия зависимостей
Зависит от абстракций. Не зависит от конкрементов 2
На этом этапе должно быть ясно, что под «абстракцией» подразумеваются интерфейсы и абстрактные классы . И должно быть понятно, почему это так. Абстракция по определению имеет дело с общим случаем. Используя абстракцию, вы упрощаете добавление новых функций или поддержку новых элементов, таких как разные базы данных или разные программы.
Конечно, вы не всегда должны использовать интерфейсы для всего, чтобы два класса не соприкасались друг с другом. Но если есть необходимость использовать абстракцию, вы также не можете позволить утечке абстракции. Вы не можете требовать, чтобы клиент интерфейса знал, что он должен использовать интерфейс определенным образом. Если вы проверяете, действительно ли интерфейс представляет определенный класс, и затем вы делаете Y вместо X, у вас есть проблема.
Выводы
Мы познакомились с основными понятиями ООП, которые необходимо знать, чтобы стать хорошим программистом. Мы увидели фундаментальные принципы проектирования, которые должны определять, как вы разрабатываете программное обеспечение. Если вы только начинаете, это может показаться немного абстрактным, но, как и все специальные знания, они необходимы для хорошего общения с вашими коллегами. И с вашим голосом, и с вашим кодом. Они имеют основополагающее значение для организации ваших знаний и практики объектно-ориентированного программирования, чтобы вы могли создавать код, который легко понятен другим людям.
Теперь, когда у вас есть прочная основа, вы можете двигаться вперед. И в целом вы наконец можете участвовать в замечательной полемике мира программирования. Я предлагаю вам начать с композиции или наследования? ,
Ссылка: | Концепции ООП (S), которые вам необходимо знать от нашего партнера по JCG |