oop概念
ØØbject P riented在AGC(OOPS或OOPS的面向对象编程系统对象)是当今使用最广泛的编程范式。 尽管大多数流行的编程语言都是多范例的,但对大型项目来说,对面向对象编程的支持至关重要。 OOP当然也有批评家。 如今,函数式编程似乎是一种新的流行方法。 尽管如此,大多数批评家仍然主要是由于对OOP的误用。
这意味着,如果您正在学习成为一名更好的程序员,那么对面向对象编程的主要概念以及它们如何工作的基本了解是至关重要的。 也许您是一位经验丰富的程序员,但是您是从实践开始的,没有任何理论背景。 或者您只是忘了在工作时更新您的知识。 您可能对实际上不知道的事情感到惊讶。 显然, 这对我们所有人来说都是可能发生的 。
因此,我们将尝试在理论与实践之间保持正确的结合,并提供大量示例。 我们的示例将基于代表团队运动的基础:我们的领域将涉及球员,教练和其他员工。 您如何表示? 我们将回答这个问题。
目录
类
每个玩家都是一个不同的人,但是他们都有共同点:他们可以执行相同的动作,例如奔跑或传球,并且具有某些特征,例如数字和位置。 教练和其他员工也可以这样说。 它们每个都有不同的组合,但是它们都遵循相同的模型。
类是描述某些功能的模型,蓝图,模板。
更准确地说,一个类通常通过称为字段的变量来表示数据,而通常由称为方法的函数表示的行为。 例如,一个类Player
可能具有一个称为Role
的字段来表示其在游戏实际字段上的角色或位置,一个Name
来表示其姓名。 作为行为,它可能具有Pass(Player)
方法,这将使他将球传给其他球员。
让我们来看一个伪代码示例。
一类
class Player
{
Text Name
Text Role
Pass (Player teamMate)
{
// body of the method
}
}
目的
如果模型中的某个班级,实际的参与者是什么? 它们被称为类的对象或实例。 在前面的示例中,参数teamMate是一个对象。 要从类中创建对象,请实例化或创建一个对象。
例如,“ Player
约翰 ”类的对象的Name
“约翰尼·约翰”, Role
“攻击者”。
Player John = new Player
John.Name = "Johnny John"
John.Role = "Attacker"
黑匣子
黑匣子是您可以观察输入和输出的东西,但是您却忽略了它的工作方式:您无法查看内部。 这可能是一件好事,因为您不必依赖包装盒中的物品。 而且,您不必在乎是否有一天有人会改变盒子内部的内容,如果它看上去仍然像从外面看到的一样。 现在,这是在OOP中应用的原理,这是一件好事。
简单来说:如果您只知道应该做什么,但不知道它是如何做的,那么您就不会搞砸它。
这个想法是将所有重要的事情委托给代码的特定部分。 这样您就可以独立于其他任何对象进行更改,而不会破坏其他内容。
例如,想象一下教练已经制定了某种策略:它不需要向球员解释如何传球或如何跑步。 它只需要告诉他们他们必须做什么。 玩家自己必须知道如何实际做这些事情。 我们希望在编程中实现相同的组织。
我们可以通过Abstraction和Encapsulation来实现。
让我们从一个普通的伪代码开始。
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
}
}
抽象化
抽象是指从类外部隐藏实现的细节。
例如, Coach
类的对象OldMan调用John的 Run()
方法,即Player
类的对象。 约翰要真正参加竞选并不需要做什么。 只需知道Player
类的对象具有方法Run()
。
封装形式
封装涉及两个概念:限制外界访问类的某些字段和方法,以及将相关的数据和方法绑定在一起。
例如,为了模拟运行的能力, Player
类有一个名为Run()
的方法,但它也有一个名为Legs
的字段。 该字段表示球员腿部的状况:他们的疲劳程度和健康状况,即是否受伤。 外界不需要知道Player
有Legs
以及Legs
如何操作。
因此,班级Player
对外界隐藏了Legs
的领域。 由于要执行Run()
因此只需要对Legs
进行操作,这样做可以确保单个对象可以完全不受外部干扰。 如果您以后想要将不同鞋子的效果添加到模拟中,这将很有用。 您只需要修改Player
类,就别无其他。
遗产
为了解决问题,您通常最终会创建以某种方式相关的类。 它们具有某些特征甚至某些行为。 由于您要避免重复,从而避免错误,因此您希望将所有这些通用功能收集在一个通用类中。 通常,此类称为父类,超类或基类。 创建此类后,其他类可以声明为类似或继承。 这称为继承。
最终结果是,从父类继承的每个类除具有自己的类外,还可以具有父类的方法和字段。
例如,您注意到Player
, Coach
和Staff
类具有名称和薪水,因此您创建了一个名为Person
的父类,并使它们从该类继承。 注意,您只需要继续创建Player
, Coach
等类的对象,而无需显式创建Person
类的对象。
class Person
{
Integer Salary
Text Name
}
class Coach : parent Person
{
AskForARaise()
{
if Salary < 1000
// request more money
}
}
在某些语言中,可以通过将Person
标记为抽象类来明确禁止创建Person
类 。 在这种情况下,您可以实际实例化的类称为具体类 。
大多数面向对象的语言都支持继承,有些还支持多重继承 :一个类可以从多个类继承。 这并非总是可能的,因为它会产生问题并增加复杂性。 一个典型的问题是,当两个不同的父类的方法具有相同的签名时,该怎么办。
通常,继承定义了两个类之间的关系is(-a-type-of)-a 。 在我们的示例中, Player
是(-a-type-of)-a Person
。
接口
接口,也称为协议,是继承的替代方法,用于使两个不相关的类彼此通信。 接口定义方法和(通常但并非总是)值。 实现接口的每个类都必须为接口的每个方法提供一个具体的方法。
例如,您要模拟弹出或解雇。 由于只能从场上踢出球员和教练,因此您不能使其成为代表人的父类的方法。 因此,您可以使用方法Ejection()
创建一个接口Ejectable
,并使Player
和Coach
实现该接口。
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
。
关联,汇总,组成
继承和接口适用于class ,但是可以使用链接两个或更多不同对象的方法。 可以按照关系松散的顺序来考虑这些方式:关联,聚合和组合。
协会
协会只是描述任何一种工作关系。 这两个对象是完全不相关的类的实例,并且没有一个对象控制另一个对象的生命周期。 他们只是为了实现自己的目标而合作。
想象一下,您想增加观众对玩家的影响,在现实生活中,观众是由人组成的,但是在我们的模拟中,他们不是Person
类的孩子。 您只想让如果Audience
类的对象HomePeople欢呼,那么John的演奏会更好。 因此, HomePeople可以影响John的行为,但是HomePeople和John都无法控制彼此的生命周期。
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) ,或反过来属于 。 在我们的例子的目的优胜者 Team
具有-A的约翰 Player
,或在逆约翰 属于对 获奖者 。
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
拥有的。 当然,在现实生活中,一旦团队决定解散竞技场,竞技场就不会神奇地消失。 但是由于我们只想模拟团队运动,因此一旦它不再归团队所有,就退出了比赛。
合成的描述就像聚合一样,因此请注意不要混淆两者。
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
}
}
多态性
在OOP的上下文中,多态意味着您可以对不同类的对象调用相同的操作,并且它们都将以自己的方式执行操作。 这是不同的,并且不应与独立于OOP的编程概念混淆:函数(方法) 重载 。 重载适用于函数,并允许您使用不同且不相关的类型定义具有相同名称的函数。 例如,您可以使用两种方法add()
:一种可以添加整数,另一种可以添加实数。
类中的多态性通常意味着方法可以对相关类的不同对象进行操作。 它在不同对象上的行为可能不同,但不一定如此。 在最基本的层面上一个子类的对象,一个是从父母继承,可以使用时,你可以使用父类的对象。 例如,您可以创建一个Interview(Person)
函数来模拟对Player
, Coach
或Staff
的采访。 不管是实际物体。 显然,要做到这一点, Interview
只能对Person
类中存在的字段起作用。
实际上,这意味着子类的对象也是父类的对象。
在某些情况下,子类可以重新定义同名父类的方法,这称为Overriding 。 在这种情况下,只要调用此方法,实际执行的方法就是子类之一。 当您需要所有子类都具有某种行为,但无法定义通用类时,此功能特别有用。 例如,您希望退休Person
类别中的所有对象退休,但Player
必须在一定年龄后退休,而Coach
或Staff
则不希望退休。
委托和公开递归
在面向对象的编程中,委托是指在另一个原始对象(发送者)的上下文中评估一个对象(接收者)的成员–来自Wikipedia
该概念在一种特殊的面向对象程序设计中被广泛使用,称为基于原型的程序设计。 使用它的一种广泛使用的语言是JavaScript。 基本思想是没有类,而只有对象。 因此,您不必从类中实例化对象,而是从通用类中克隆它们,然后修改其原型以适合您的需求。
尽管它是最著名的编程语言的核心,却鲜为人知。 实际上,即使是JavaScript,它也通常以面向对象编程的常用样式使用。 尽管它看起来像是不可思议的知识,但重要的是要知道它,因为它已广泛用于称为this或self的特殊变量或关键字。
大多数面向对象的编程语言都支持它,并且它允许从类或任何子类中定义的方法内部引用将实例化的特定对象(而不是类)。 当代码运行的事实, this
将涉及到一个具体的目标是什么让开放递归 。 这意味着基类可以定义使用this
方法引用其方法之一的方法,实际对象将使用该方法引用具有相同签名的子方法。
这听起来很复杂,但事实并非如此。 想象下面的伪代码。
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()
在最后一行是发生开放递归的地方,因为方法DoesHaveToRetire
是在使用this.IsOld()
的父类中定义的方法(第16行)。 但是实际上在运行时调用的IsOld
方法是子类Player
定义的方法。
这也是委派,因为在第16行中, this
是在对象John的上下文中评估的,被评估为Player
类的对象,而不是原始的John的此对象(作为Person
类的对象)。 因为请记住, John既是Player
类的对象,也是其父类Person
。
设计不良的症状
到目前为止,我们已经讨论了基础知识。 我们认为有些人需要更多的知识来有效地应用这些知识。 首先,我们需要查看不良设计的症状,以便可以在代码中检测到它们。
刚性
即使是很小的事情,该软件也很难更改。 每次修改都需要级联的更改,而这些更改需要数周的时间才能应用。 开发人员本身不知道在需要执行X或Y时会发生什么以及必须更改什么。这导致开发人员和管理人员都不愿更改。 这使代码很难维护。
易碎性
每次更改该软件都会以意外的方式中断。 这是与刚度相关的问题,但是却有所不同,因为没有一个修改序列会持续增加一个小时。 一切似乎都可行,但是当您认为准备将代码交付测试或什至更糟的客户时,就会告诉您其他问题不再起作用。 新事物起作用,但是另一事物坏了。 实际上,每个修复程序都是两个新问题。 这导致开发人员中存在生存恐惧,感觉它失去了对软件的控制。
不可移植性
每个模块都可以工作,但只有在谨慎的情况下才可以使用。您不能在另一个项目中重复使用代码,因为它们太多了,您需要更改。 该程序似乎可以通过黑魔法起作用。 没有设计,只有黑客。 每次修改某样东西时,您都知道该怎么做,但这总是一件可怕的事情,使您担心它会再次咬住您。 而且会的。 除了感到羞耻之外,您还应该真正感受到它使代码难以重用。 相反,您将重新创建稍有不同的代码,几乎可以完成相同的操作。
良好设计原则
要知道问题是什么样的,还不够。 我们还需要了解实用的设计原则,以便能够一开始就避免创建糟糕的设计。 这一众所周知的原则是多年经验的结晶,并以其缩写词SOLID闻名。
单一责任
改变班级的理由不应该只有一个以上1
通常,变更原因在“责任”中进行了修改,因此使用了原则的名称,但这是原始的表述。 在这种情况下,责任或变更理由取决于特定项目及其要求。 显然,这并不意味着一个类只能有一个方法。 这意味着,当您描述它所做的事情时,就说它仅做这一件事。 如果您违反了此原则,则不同的责任会变得耦合,并且出于许多不同的原因,您可能不得不更改同一班级或多个班级。
让我们回到我们的例子。 您需要保持比赛得分。 您可能很想使用Player
类,毕竟,玩家可以得分。 但是,如果您这样做了,那么每次您需要知道分数时,也需要与播放器进行交互。 如果需要使一个点无效,该怎么办? 每个班级要保留一份工作。
打开关闭
模块应打开以进行扩展,但应关闭以进行修改2
在这种情况下,模块是指照顾软件一个目标的一个或多个类。 此原则意味着您必须能够添加对新项目的支持,而不必更改模块本身的代码。 例如,您必须能够在不更改Player
类的情况下添加一种新型的播放器(例如,守门员)。
这使开发人员可以支持新事物,这些事物执行与您已经拥有的事物相同的功能,而不必为此提供“特殊情况”。
里斯科夫换人
子类必须可用作其基类。 2
请注意,这不是原始公式,因为它太数学了。 这是一个重要的原则,已将其纳入面向对象语言本身的设计中。 但是语言本身只保证原则的一部分,形式部分。 从技术上讲,您始终可以像使用基类的对象一样使用子类的对象,但是对我们来说重要的还是实际用法。
这意味着即使重写方法,也不应实质性地修改子类的行为。 例如,如果要进行赛车游戏,则不能为在水下移动的汽车创建子类。 如果这样做, Move()
方法的行为将与基类不同。 几个月后,将会出现一堆奇怪的小虫子,它们把海湾流当作高速公路。
接口隔离
许多客户端特定的接口优于一个通用接口2
在本文中,“特定于客户”是指针对每种类型的客户,而不是针对每个客户类别。 该原则表明,您不应为确实做非常不同的事情的客户端实现一个通用接口。 那是因为这使每种类型的客户端彼此耦合。 如果修改一种类型的客户端,则必须修改常规界面,也许还必须修改其他客户端。 如果您在接口上有多个特定于一个客户端的方法,则可以识别出违反了该原理。 这可能是因为他们以一种不同的,特定的方式来做通常的事情,也可能是他们做了一个客户只需要做的事情。
在实际使用中,这可能更难遵守。 尤其是因为一开始很容易错过实际上是不同的需求。 例如,假设您必须处理连接:电缆连接,移动连接等。它们都是相同的,不是吗?
嗯,理论上它们的行为方式相同,但实际上它们可能有所不同。 移动连接通常比较昂贵,并且它们对每月传输的数据的大小有严格的限制。 因此,用于移动连接的兆字节要比用于电缆的兆字节珍贵。 结果,您可能会发现自己检查数据的频率降低了,或者使用了SendBasicData()
而不是SendData()
…除非您已经在特定领域有经验,否则可能最终会强迫自己遵循这一原则。
依赖倒置
取决于抽象。 不依赖混凝土2
在这一点上应该很清楚,“抽象”是指接口和抽象类 。 而且还应该清楚为什么它是真的。 根据定义,抽象处理一般情况。 通过使用抽象,您可以更轻松地添加新功能或支持新项目,例如不同的数据库或不同的程序。
当然,您不应总是对所有事物都使用接口,以免两个类相互接触。 但是,如果需要使用抽象,那么您也不能让抽象泄漏。 您不能要求接口客户端知道它必须以某种方式使用接口。 如果您发现自己在检查一个接口是否确实代表某个类,然后执行Y而不是X,那么您就遇到了问题。
结论
我们已经了解了成为一名优秀程序员需要了解的基本OOP概念。 我们已经看到了必须指导您如何开发软件的基本设计原则。 如果您刚开始,这些内容可能看起来有点抽象,但是像所有专门知识一样,它们也需要与您的同事进行良好的沟通。 既有声音又有代码。 它们是组织您的面向对象编程知识和实践的基础,因此您可以创建易于他人理解的代码。
现在您已经有了坚实的基础,就可以继续前进。 总体而言,您最终可以参与编程世界的精彩辩论。 我建议您从撰写还是继承开始? 。
翻译自: https://www.javacodegeeks.com/2017/05/oops-concepts-need-know.html
oop概念