一、含义
面向对象思想是一种编程范式,它将系统中的实体看作对象,并使用类和对象来组织代码。在Unity中,面向对象思想也得到了广泛的应用。具体来说,以下是面向对象思想在Unity中的应用:
- 组件化:在Unity中,每个物体都是由多个组件组成的,例如Transform、Rigidbody、Collider等。这种组件化的设计方式就是面向对象思想的体现,通过组合不同的组件来实现不同的行为和功能,提高了代码的可维护性和灵活性。
- 继承和多态:Unity中的组件都是从MonoBehaviour类中派生出来的,这也就是继承的应用。同时,在组件之间也存在多态的关系,例如脚本可以派生出不同的子类,实现不同的行为和功能。
- 封装和抽象:在Unity中,对于组件的实现通常是通过封装和抽象来实现的。例如,开发者可以封装一些公共的代码和逻辑成为一个类,并提供对外的接口来实现不同的行为和功能。
- 设计模式:许多常见的设计模式也得到了广泛的应用,例如单例模式、工厂模式、策略模式、观察者模式等,这些设计模式可以帮助开发者更好地组织代码和处理复杂的逻辑关系。
二、五大设计原则
面向对象的五大基本设计原则也被称为SOLID原则,是一组代码设计原则,旨在帮开发者编写可维护、可重用、可扩展的代码。也有些说法认为面向对象具有七大基本原则,但在小编看来,只要实现了五大基本原则,剩下的两个原则也自然会符合,故下面直接介绍五大基本原则:
1、单一职责性原则(Single Responsibility Principle,SRP):一个类或方法应该只有一个职责。也就是说,只负责一个特定的功能或行为。
2、开放封闭原则(Open-Closed Principle,OCP):一个类或方法应该对扩展开放,对修改封闭。也就是说,应该通过扩展已有的代码来添加新功能,而不是修改原有代码。
3、里氏替换原则(Liskov Substitution Principle,LSP):子类应该能替换他们的父类,并且在任何情况下不会违反程序的正确性。也就是说,子类的接口和行为应该和父类相同,尽管具体的实现可能不同。
4、接口隔离原则(Interface Segregation Principle,ISP):多个客户端接口优于一个宽泛的接口。也是说,应该将一个大型的接口拆分成多个较小的客户端接口,以便客户端只需要实现自己需要的接口,而不需要实现自己不需要的接口。
5、依赖反转原则(Dependency Inversion Principle,DIP): 高层模块不应该依赖低层模块,二者都应该依赖抽象接口。也就是说,应该通过接口而非具体实现来定义模块之间的依赖关系,已实现代码的灵活性和扩展性。
三、五大设计原则在unity场景中的实现
光是说概念,想要真正理解SOLID原则不是一件简单的事情,因此为了帮助大家理解这个抽象的概念,也为了让面向对象的思想能够真正用在开发中,小编下面会通过一些unity开发代码示例,用正反举例法来结合解释SOLID原则:
1、单一职责原则:
违反单一职责原则的情况(职责不单一):
public class Player : MonoBehaviour { public void Move() { // 处理玩家移动逻辑 } public void PlayWalkAnimation() { // 播放玩家行走动画 } }
在这个例子中,Player类包含了处理玩家移动和播放行走动画的方法。这样的设计使得Player类承担了多个职责,职责不单一,违反了单一职责原则。
符合单一职责原则的情况(职责单一):
public class PlayerMovement : MonoBehaviour { public void Move() { // 处理玩家移动逻辑 } } public class PlayerAnimation : MonoBehaviour { public void PlayWalkAnimation() { // 播放玩家行走动画 } }
在这个例子中,PlayerMovement类负责处理玩家移动逻辑,而PlayerAnimation类负责处理玩家动画逻辑。每个类都只关注自己的职责,职责单一,遵循了单一职责原则。
每个类应该只负责一个单一的职责,这样可以提高代码的可读性、可维护性和可扩展性。如果一个类承担了多个职责,那么当其中一个职责发生变化时,可能会影响到其他职责,导致代码的脆弱性和复杂性增加。因此,遵循单一职责原则能够有效地管理类的职责,并使代码更加灵活和可维护。
2、开放封闭原则
违反开闭原则的情况:(开放修改原有代码)
public class Enemy : MonoBehaviour { public void Attack(string enemyType) { if (enemyType == "basic") { // 实现基础敌人的攻击逻辑 } else if (enemyType == "boss") { // 实现boss敌人的攻击逻辑 } } }
在这个例子中,Enemy类的Attack方法接受一个字符串参数enemyType,根据该参数来判断执行不同类型敌人的攻击逻辑。如果需要新增一种新的敌人类型,就需要修改Enemy类的Attack方法,违反了开闭原则。
符合开放封闭原则的情况:(开放扩展而非修改)
public abstract class Enemy : MonoBehaviour { public abstract void Attack(); } public class BasicEnemy : Enemy { public override void Attack() { // 实现基础敌人的攻击逻辑 } } public class BossEnemy : Enemy { public override void Attack() { // 实现boss敌人的攻击逻辑 } }
在这个例子中,Enemy类是一个抽象类,定义了攻击的抽象方法。BasicEnemy和BossEnemy是具体的敌人类,分别实现了攻击方法。如果需要新增一种新的敌人类型,只需继承Enemy类并实现攻击方法即可,而无需修改已有的代码。这样遵循了开闭原则,对扩展开放,对修改关闭。
通过使用抽象类和多态性,可以尽量避免对已有代码的修改,而是通过添加新的代码来扩展功能。这样可以减少引入错误的风险,并提高代码的可维护性和可扩展性。
3、里氏替换原则
违反里氏替换原则的情况:(子类类型需要符合某种限制,不能完全替代父类)
public class Animal { } public class Dog : Animal { } public class Cat : Animal { } // 使用父类对象的地方 public class AnimalController { public void MakeAnimalSound(Animal animal) { if (animal is Dog) { Debug.Log("Woof woof!"); } else if (animal is Cat) { Debug.Log("Meow meow!"); } else { Debug.Log("Animal sound!"); } } }
通过判断对象的具体类型来播放相应的声音。这种设计违反了里氏替换原则,因为在使用Animal类型的对象时,需要根据具体类型做额外的判断和处理。
符合里氏替换原则的情况:(没有限制,子类完全可以替代父类)
public abstract class Animal : MonoBehaviour { public abstract void MakeSound(); } public class Dog : Animal { public override void MakeSound() { Debug.Log("Woof woof!"); } } public class Cat : Animal { public override void MakeSound() { Debug.Log("Meow meow!"); } } // 使用父类对象的地方 public class AnimalController : MonoBehaviour { public void MakeAnimalSound(Animal animal) { animal.MakeSound(); } }
在这个例子中,Animal是一个抽象类,定义了动物的抽象方法MakeSound。Dog和Cat是具体的动物类,分别实现了MakeSound方法。AnimalController是一个控制器类,其中的MakeAnimalSound方法接受Animal类型的参数,通过调用MakeSound方法播放动物的声音。在这里,我们可以使用Dog或Cat的实例作为Animal类型的参数,并且不会产生错误或异常行为。这符合里氏替换原则,子类对象(Dog、Cat)可以替换父类对象(Animal)。
子类对象应该能够替换父类对象,并且在使用父类对象的地方不会产生错误或异常行为。只有当子类能够完全满足父类的行为约定时,才能真正实现里氏替换原则。
4、接口隔离原则
违反接口隔离原则:(所有行为写在一个接口中)
// 接口隔离前 public interface ICharacter { void Move(); void Attack(); void Defend(); } public class Player : ICharacter { public void Move() { // 玩家移动逻辑 } public void Attack() { // 玩家攻击逻辑 } public void Defend() { // 玩家防御逻辑 } public void UseItem() { // 玩家使用物品逻辑 } } public class Enemy : ICharacter { public void Move() { // 敌人移动逻辑 } public void Attack() { // 敌人攻击逻辑 } public void Defend() { // 敌人防御逻辑 } public void Heal() { // 敌人治疗逻辑 } }
在这个例子中,ICharacter接口定义了Move、Attack和Defend方法。但是Player类在实现ICharacter接口的同时,添加了UseItem方法,而Enemy类在实现ICharacter接口的同时,添加了Heal方法。这样的设计违反了接口隔离原则,因为客户端(使用Player或Enemy对象的地方)可能会依赖于不需要的接口方法,导致代码的耦合度增加。
符合接口隔离原则:(多个行为写在多个接口中)
public interface IMovable { void Move(); } public interface IAttackable { void Attack(); } public interface IDefendable { void Defend(); } public class Player : IMovable, IAttackable, IDefendable { public void Move() { // 玩家移动逻辑 } public void Attack() { // 玩家攻击逻辑 } public void Defend() { // 玩家防御逻辑 } } public class Enemy : IMovable, IAttackable, IDefendable { public void Move() { // 敌人移动逻辑 } public void Attack() { // 敌人攻击逻辑 } public void Defend() { // 敌人防御逻辑 } }
在这个修正后的例子中,我们将ICharacter接口拆分为IMovable、IAttackable和IDefendable接口,每个接口只包含一个具体的方法。Player和Enemy类实现了相应的接口,根据自己的角色类型实现了对应的方法。这样,客户端可以根据需要依赖相应的接口,而不会依赖不需要的方法,符合接口隔离原则。
接口应该根据客户端的需求进行划分,将接口拆分为更小、更具体的接口,使得客户端只需要依赖所需的接口方法,从而降低耦合度。
5、依赖反转原则
违反依赖反转原则的情况:(高层模块直接依赖底层模块)
public class Gun { public void Attack() { // 枪支攻击逻辑 } } public class Sword { public void Attack() { // 剑攻击逻辑 } } public class Player { private Gun gun; public Player() { gun = new Gun(); } public void Attack() { gun.Attack(); } }
在这个例子中,Player类直接依赖于具体的Gun类。Player类在构造函数中实例化Gun对象,使得高层模块Player和低层模块Gun之间产生了直接的依赖关系。这样的设计违反了依赖反转原则,因为高层模块依赖于低层模块,而没有依赖于抽象。
符合依赖反转原则的情况:(高层模块、底层模块都依赖抽象接口)
public interface IWeapon { void Attack(); } public class Gun : IWeapon { public void Attack() { // 枪支攻击逻辑 } } public class Sword : IWeapon { public void Attack() { // 剑攻击逻辑 } } public class Player { private IWeapon weapon; public Player(IWeapon weapon) { this.weapon = weapon; } public void Attack() { weapon.Attack(); } }
在这个例子中,IWeapon是一个抽象的武器接口,Gun和Sword是具体的武器类,它们都实现了IWeapon接口。Player类作为高层模块,通过构造函数接收IWeapon类型的参数,不直接依赖于具体的武器类,而是依赖于抽象的接口。这样,可以实现依赖的反转,高层模块Player依赖于抽象IWeapon,而不是具体的Gun或Sword。
高层模块应该依赖于抽象而不是具体实现,通过依赖注入等方式将具体实现的创建和管理交给外部,从而实现模块间的解耦和灵活性。
后记:
希望上面的实际例子能够帮助大家更好的去约束自己平时的编程习惯,提升个人代码质量,打造出让老板放心,让自己省心的牛逼哄哄的代码!以上内容都是小编个人原创理解,如果有大佬对于内容有不同的见解,请轻喷(;∀;)。如有转载,请注明出处。