Code Quality Design Help

Deep Inheritance—Unity's MonoBehaviour

Note: This article demonstrates problematic inheritance practices that violate SOLID principles. This is presented as an educational example of what to avoid in your own code design.

Context: Why This Exists (And Why You Shouldn't Copy It)

Unity's MonoBehaviour inheritance hierarchy represents a real-world example of deep inheritance that, while necessary for the engine's architecture, demonstrates several design principles that should generally be avoided in application code. This inheritance hierarchy exists in Unity's engine code for specific architectural reasons:

  • Performance: Direct access to native C++ engine code through inheritance

  • Legacy: Years of API evolution and backward compatibility requirements

  • Framework Requirements: Unity needs a unified lifecycle management system

Important: This is library/framework code that you consume, not application code that you should design this way.

The Hierarchy and Its Issues

The diagram at the end of this article shows the inheritance chain from ObjectComponentBehaviourMonoBehaviour. Each level adds significant functionality and complexity:

Object Class

  • Base class for all Unity entities

  • Handles object lifecycle (creation, destruction)

  • Manages serialization

  • Provides object finding capabilities

  • Contains dozens of methods and properties

Component Class

  • Adds GameObject relationship

  • Provides component query methods (GetComponent, etc.)

  • Implements messaging system (SendMessage)

  • Adds parent-child hierarchy navigation

Behaviour Class

  • Introduces enabled/disabled state

  • Manages activation status

MonoBehaviour Class

  • Adds coroutine support

  • Implements lifecycle methods (Start, Update, etc.)

  • Handles script execution timing

  • Provides invoke system (delayed method calls)

SOLID Principle Violations

Single Responsibility Principle (SRP)

Each class in this hierarchy has multiple responsibilities:

  • Object: Handles instantiation, destruction, finding objects, lifecycle management, and native interop

  • Component: Manages component relationships, GameObject references, messaging, and component queries

  • MonoBehaviour: Adds coroutines, invoke scheduling, GUI layout, and script lifecycle management

Open/Closed Principle (OCP)

This deep inheritance hierarchy makes extending behavior difficult:

  • New Unity features often require changes throughout the hierarchy

  • Developers must inherit the entire implementation stack, even for simple functionality

Liskov Substitution Principle (LSP)

When inheriting from MonoBehaviour:

  • You inherit massive amounts of implementation

  • It's easy to violate LSP by overriding methods in ways that break expected behavior

  • The "Awake before Start before Update" lifecycle constraints create fragile dependencies

Interface Segregation Principle (ISP)

The most obvious violation in this hierarchy:

  • MonoBehaviour exposes dozens of methods and properties

  • Most scripts use only a tiny fraction of the inherited API surface

  • Every script is forced to inherit this massive interface

Dependency Inversion Principle (DIP)

The hierarchy creates tight coupling:

  • Scripts depend directly on concrete implementation details

  • Cannot easily substitute alternative implementations

  • Testing becomes difficult due to engine dependencies

Why Didn't Unity Choose Composition?

Unity's designers had several constraints that led to this inheritance-heavy approach:

Historical Reasons

  • Unity began development in the early 2000s when inheritance was more favored

  • C++ background of engine developers influenced the design

  • Unity adopted a single, intuitive base class (MonoBehaviour) without requiring explicit overrides or using composition, matching how beginners naturally structure code. (TBD) (This point is the one I spent the most time on. And I don't have any evidence to back it up, but it just feels right.)

Technical Constraints

  • Need for direct native code interop

  • Serialization system requirements

  • Editor integration demands

This would provide better separation of concerns but would require a completely different engine architecture.

Lessons for Your Own Code

What NOT to Do

  • Don't create deep inheritance hierarchies like this in your application code

  • Don't put multiple responsibilities in base classes

  • Don't inherit just for code reuse (favor composition instead)

  • Don't force clients to depend on functionality they don't need

What TO Do Instead

  • Favor composition over inheritance for your game scripts

  • Use interfaces to define contracts (like ISwitchable in our other examples)

  • Keep inheritance shallow—prefer 2–3 levels maximum

  • Use inheritance for "is-a" relationships only

Working WITH Unity's Inheritance

Since you must inherit from MonoBehaviour, follow these guidelines:

// Good: Focused responsibility public class HealthDisplay : MonoBehaviour { [SerializeField] private HealthComponent healthComponent; [SerializeField] private Text healthText; void Update() { healthText.text = $"Health: {healthComponent.CurrentHealth}"; } } // Bad: Multiple responsibilities inherited and extended public class Player : MonoBehaviour { // Movement public float moveSpeed = 5f; // Health public int health = 100; // Inventory public List<Item> inventory = new List<Item>(); // Combat public void Attack() { /* ... */ } // Input handling void Update() { /* Handle all player input */ } // Don't put all game logic directly in MonoBehaviour // Instead, compose with other classes }

Deep Dive: Specific Problems

Massive Interface Surface

The MonoBehaviour class exposes dozens of methods and properties. Every class that inherits from it gets:

  • 50+ methods from Object (Instantiate, Destroy, FindObjectsOfType, etc.)

  • 20+ methods from Component (GetComponent, SendMessage, etc.)

  • 15+ methods from MonoBehaviour (StartCoroutine, Invoke, etc.)

This violates the Interface Segregation Principle—most scripts don't need 90% of these methods.

Hidden Dependencies

When you inherit from MonoBehaviour, you implicitly depend on:

  • Unity's serialization system

  • The GameObject/Component architecture

  • Unity's execution order system

  • The native engine runtime

  • Editor integration systems

This makes testing difficult and creates tight coupling.

Violation of Liskov Substitution Principle

It's very easy to create MonoBehaviour subclasses that aren't truly substitutable:

// This violates LSP - requires specific setup public class DatabaseConnection : MonoBehaviour { void Start() { // Assumes specific GameObject setup // Breaks if used differently } }

Takeaways

  1. Library code has different constraints than application code

  2. Unity's inheritance hierarchy serves specific architectural needs but shouldn't be emulated

  3. When forced to inherit (like from MonoBehaviour), keep your classes focused and use composition internally

  4. Learn from both good and bad examples - this is a case study in what not to do in your own designs

Understanding why this code exists helps you make better decisions in your own object-oriented designs. See Also:

UML Diagram MonoBehaviour Inheritance

ObjectkInstanceID_None: const intm_CachedPtr: IntPtrm_InstanceID: intm_UnityRuntimeErrorString: stringobjectIsNullMessage: const stringcloneDestroyedMessage: const string name: stringhideFlags: HideFlags  AsyncInstantiateOperation<T>AsyncInstantiateOperation<T>AsyncInstantiateOperation<T>AsyncInstantiateOperation<T>AsyncInstantiateOperation<T>positions: ReadOnlySpan<Vector3>,AsyncInstantiateOperation<T>AsyncInstantiateOperation<T>parent: Transform, position: Vector3, rotation: Quaternion,AsyncInstantiateOperation<T>parent: Transform, positions: ReadOnlySpan<Vector3>,AsyncInstantiateOperation<T>parent: Transform, positions: ReadOnlySpan<Vector3>,rotations: ReadOnlySpan<Quaternion>,AsyncInstantiateOperation<T>parameters: InstantiateParameters,AsyncInstantiateOperation<T>parameters: InstantiateParameters,AsyncInstantiateOperation<T>rotation: Quaternion, parameters: InstantiateParameters,AsyncInstantiateOperation<T>position: Vector3, rotation: Quaternion,parameters: InstantiateParameters,AsyncInstantiateOperation<T>positions: ReadOnlySpan<Vector3>,rotations: ReadOnlySpan<Quaternion>,parameters: InstantiateParameters,AsyncInstantiateOperation<T> Object findObjectsInactive: FindObjectsInactive,T[]position: Vector3, rotation: Quaternion,count: int, parameters: InstantiateParameters,positions: IntPtr, positionsCount: int, rotations: IntPtr,IntPtrHideFlagsdata: IntPtr, ref position: Vector3, ref rotation: Quaternion,original: IntPtr, count: int, ref parameters:InstantiateParameters, positions: IntPtr, positionsCount: int,rotations: IntPtr, rotationsCount: int,data: IntPtr, parent: IntPtr, ref pos: Vector3,IntPtrIntPtrGetInstanceID(): intGetHashCode(): intEquals(other: object): boolop_Implicit(exists: Object): boolCompareBaseObjects(lhs: Object, rhs: Object): boolEnsureRunningOnMainThread(): voidIsNativeObjectAlive(o: Object): boolGetCachedPtr(): IntPtr InstantiateAsync<T>(original: T):InstantiateAsync<T>(original: T, parent: Transform):InstantiateAsync<T>(original: T, position: Vector3,rotation: Quaternion): AsyncInstantiateOperation<T>InstantiateAsync<T>(original: T, parent: Transform,position: Vector3, rotation: Quaternion):InstantiateAsync<T>(original: T, count: int):InstantiateAsync<T>(original: T, count: int,parent: Transform): AsyncInstantiateOperation<T>InstantiateAsync<T>(original: T, count: int,position: Vector3, rotation: Quaternion):InstantiateAsync<T>(original: T, count: int,rotations: ReadOnlySpan<Quaternion>):InstantiateAsync<T>(original: T, count: int,parent: Transform, position: Vector3, rotation: Quaternion):InstantiateAsync<T>(original: T, count: int,cancellationToken: CancellationToken):InstantiateAsync<T>(original: T, count: int,rotations: ReadOnlySpan<Quaternion>):InstantiateAsync<T>(original: T, count: int,cancellationToken: CancellationToken):InstantiateAsync<T>(original: T,cancellationToken: CancellationToken):InstantiateAsync<T>(original: T, count: int,cancellationToken: CancellationToken):InstantiateAsync<T>(original: T, position: Vector3,cancellationToken: CancellationToken):InstantiateAsync<T>(original: T, count: int,cancellationToken: CancellationToken):InstantiateAsync<T>(original: T, count: int,cancellationToken: CancellationToken):Instantiate(original: Object, position: Vector3,rotation: Quaternion): ObjectInstantiate(original: Object, position: Vector3,rotation: Quaternion, parent: Transform): ObjectInstantiate(original: Object): ObjectInstantiate(original: Object, scene: Scene): ObjectInstantiate<T>(original: T,parameters: InstantiateParameters): TInstantiate<T>(original: T, position: Vector3,rotation: Quaternion, parameters: InstantiateParameters): TInstantiate(original: Object, parent: Transform):Instantiate(original: Object, parent: Transform,instantiateInWorldSpace: bool): ObjectInstantiate<T>(original: T): TInstantiate<T>(original: T, position: Vector3,rotation: Quaternion): TInstantiate<T>(original: T, position: Vector3,rotation: Quaternion, parent: Transform): TInstantiate<T>(original: T, parent: Transform): TInstantiate<T>(original: T, parent: Transform,worldPositionStays: bool): T Destroy(obj: Object, t: float): voidDestroy(obj: Object): voidDestroyImmediate(obj: Object,allowDestroyingAssets: bool): voidDestroyImmediate(obj: Object): voidFindObjectsOfType(type: System.Type):Object[] (Obsolete)FindObjectsOfType(type: System.Type,includeInactive: bool): Object[] (Obsolete)FindObjectsByType(type: System.Type,sortMode: FindObjectsSortMode): Object[]FindObjectsByType(type: System.Type,sortMode: FindObjectsSortMode): Object[]DontDestroyOnLoad(target: Object): voidDestroyObject(obj: Object, t: float): void (Obsolete)DestroyObject(obj: Object): void (Obsolete)FindSceneObjectsOfType(type: System.Type):Object[] (Obsolete)FindObjectsOfTypeIncludingAssets(type: System.Type):Object[] (Obsolete)FindObjectsOfType<T>(): T[] (Obsolete)FindObjectsByType<T>(sortMode: FindObjectsSortMode):FindObjectsOfType<T>(includeInactive: bool):T[] (Obsolete)FindObjectsByType<T>(findObjectsInactive:FindObjectsInactive, sortMode: FindObjectsSortMode): T[]FindObjectOfType<T>(): T (Obsolete)FindObjectOfType<T>(includeInactive: bool):T (Obsolete)FindFirstObjectByType<T>(): TFindAnyObjectByType<T>(): TFindFirstObjectByType<T>(findObjectsInactive:FindObjectsInactive): TFindAnyObjectByType<T>(findObjectsInactive:FindObjectsInactive): TFindObjectsOfTypeAll(System.Type type):Object[] (Obsolete)CheckNullArgument(arg: object, message: string): voidFindObjectOfType(type: System.Type): Object (Obsolete)FindFirstObjectByType(type: System.Type): ObjectFindAnyObjectByType(type: System.Type): ObjectFindObjectOfType(type: System.Type,includeInactive: bool): Object (Obsolete)FindFirstObjectByType(type: System.Type,findObjectsInactive: FindObjectsInactive): ObjectFindAnyObjectByType(type: System.Type,findObjectsInactive: FindObjectsInactive): ObjectToString(): stringop_Equality(x: Object, y: Object): boolop_Inequality(x: Object, y: Object): boolGetOffsetOfInstanceIDInCPlusPlusObject(): intCurrentThreadIsMainThread(): boolInternal_CloneSingle(data: Object): ObjectInternal_CloneSingleWithScene(data: Object,scene: Scene): ObjectInternal_CloneSingleWithParams(data: Object,parameters: InstantiateParameters): ObjectInternal_InstantiateSingleWithParams(data: Object,parameters: InstantiateParameters): ObjectInternal_CloneSingleWithParent(data: Object,parent: Transform, worldPositionStays: bool): ObjectInternal_InstantiateAsyncWithParams(original: Object,rotationsCount: int, hasManagedCancellationToken: bool):Internal_InstantiateSingle(data: Object,pos: Vector3, rot: Quaternion): ObjectInternal_InstantiateSingleWithParent(data: Object,parent: Transform, pos: Vector3, rot: Quaternion): ObjectToString(obj: Object): stringGetName(): stringIsPersistent(obj: Object): boolSetName(name: string): voidDoesObjectWithInstanceIDExist(instanceID: int): boolFindObjectFromInstanceID(instanceID: int): ObjectGetPtrFromInstanceID(instanceID: int,objectType: System.Type, out isMonoBehaviour: bool): IntPtrForceLoadFromInstanceID(instanceID: int): ObjectCreateMissingReferenceObject(instanceID: int): ObjectMarkDirty(): voidDestroy_Injected(obj: IntPtr, t: float): voidDestroyImmediate_Injected(obj: IntPtr,allowDestroyingAssets: bool): voidDontDestroyOnLoad_Injected(target: IntPtr): voidget_hideFlags_Injected(_unity_self: IntPtr):set_hideFlags_Injected(_unity_self: IntPtr,value: HideFlags): voidInternal_CloneSingle_Injected(data: IntPtr): IntPtrInternal_CloneSingleWithScene_Injected(data: IntPtr,ref scene: Scene): IntPtrInternal_CloneSingleWithParams_Injected(data: IntPtr,ref parameters: InstantiateParameters): IntPtrInternal_InstantiateSingleWithParams_Injected(ref parameters: InstantiateParameters): IntPtrInternal_CloneSingleWithParent_Injected(data: IntPtr,parent: IntPtr, worldPositionStays: bool): IntPtrInternal_InstantiateAsyncWithParams_Injected(hasManagedCancellationToken: bool): IntPtrInternal_InstantiateSingle_Injected(data: IntPtr,ref pos: Vector3, ref rot: Quaternion): IntPtrInternal_InstantiateSingleWithParent_Injected(ref rot: Quaternion): IntPtrToString_Injected(obj: IntPtr,out ret: ManagedSpanWrapper): voidGetName_Injected(_unity_self: IntPtr,out ret: ManagedSpanWrapper): voidIsPersistent_Injected(obj: IntPtr): boolSetName_Injected(_unity_self: IntPtr,ref name: ManagedSpanWrapper): voidFindObjectFromInstanceID_Injected(instanceID: int):ForceLoadFromInstanceID_Injected(instanceID: int):MarkDirty_Injected(_unity_self: IntPtr): voidComponenttransform: TransformgameObject: GameObjecttag: stringrigidbody: Component (Obsolete)rigidbody2D: Component (Obsolete)camera: Component (Obsolete)light: Component (Obsolete)animation: Component (Obsolete)constantForce: Component (Obsolete)renderer: Component (Obsolete)audio: Component (Obsolete)networkView: Component (Obsolete)collider: Component (Obsolete)collider2D: Component (Obsolete)hingeJoint: Component (Obsolete)particleSystem: Component (Obsolete) GetComponent(type: System.Type): ComponentGetComponentFastPath(type: System.Type,oneFurtherThanResultValue: IntPtr): voidGetComponent<T>(): TTryGetComponent(type: System.Type,out component: Component): boolTryGetComponent<T>(out component: T): boolGetComponent(type: string): ComponentGetComponentInChildren(t: System.Type,includeInactive: bool): ComponentGetComponentInChildren(t: System.Type): ComponentGetComponentInChildren<T>(includeInactive: bool): TGetComponentInChildren<T>(): TGetComponentsInChildren(t: System.Type,includeInactive: bool): Component[]GetComponentsInChildren(t: System.Type): Component[]GetComponentsInChildren<T>(includeInactive: bool): T[]GetComponentsInChildren<T>(includeInactive: bool,result: List<T>): voidGetComponentsInChildren<T>(): T[]GetComponentsInChildren<T>(results: List<T>): voidGetComponentInParent(t: System.Type,includeInactive: bool): ComponentGetComponentInParent(t: System.Type): ComponentGetComponentInParent<T>(includeInactive: bool): TGetComponentInParent<T>(): TGetComponentsInParent(t: System.Type,includeInactive: bool): Component[]GetComponentsInParent(t: System.Type): Component[]GetComponentsInParent<T>(includeInactive: bool): T[]GetComponentsInParent<T>(includeInactive: bool,results: List<T>): voidGetComponentsInParent<T>(): T[]GetComponents(type: System.Type): Component[]GetComponentsForListInternal(searchType: System.Type,resultList: object): voidGetComponents(type: System.Type,results: List<Component>): voidGetComponents<T>(results: List<T>): voidGetComponents<T>(): T[]GetComponentIndex(): intCompareTag(tag: string): boolCompareTag(tag: TagHandle): boolGetCoupledComponent(): ComponentIsCoupledComponent(): boolSendMessageUpwards(methodName: string, value: object,options: SendMessageOptions): voidSendMessageUpwards(methodName: string, value: object): voidSendMessageUpwards(methodName: string): voidSendMessageUpwards(methodName: string,options: SendMessageOptions): voidSendMessage(methodName: string, value: object): voidSendMessage(methodName: string): voidSendMessage(methodName: string, value: object,options: SendMessageOptions): voidSendMessage(methodName: string,options: SendMessageOptions): voidBroadcastMessage(methodName: string, parameter: object,options: SendMessageOptions): voidBroadcastMessage(methodName: string, parameter: object): voidBroadcastMessage(methodName: string): voidBroadcastMessage(methodName: string,options: SendMessageOptions): void get_transform_Injected(_unity_self: IntPtr): IntPtrget_gameObject_Injected(_unity_self: IntPtr): IntPtrGetComponentFastPath_Injected(_unity_self: IntPtr, type: System.Type, oneFurtherThanResultValue: IntPtr): voidGetComponent_Injected(_unity_self: IntPtr, ref type: ManagedSpanWrapper): IntPtrGetComponentsForListInternal_Injected(_unity_self: IntPtr, searchType: System.Type, resultList: object): voidGetComponentIndex_Injected(_unity_self: IntPtr): intGetCoupledComponent_Injected(_unity_self: IntPtr): IntPtrIsCoupledComponent_Injected(_unity_self: IntPtr): boolSendMessageUpwards_Injected(_unity_self: IntPtr, ref methodName: ManagedSpanWrapper, value: object, options: SendMessageOptions): voidSendMessage_Injected(_unity_self: IntPtr, ref methodName: ManagedSpanWrapper, value: object, options: SendMessageOptions): voidBroadcastMessage_Injected(_unity_self: IntPtr, ref methodName: ManagedSpanWrapper, parameter: object, options: SendMessageOptions): voidBehaviourenabled: boolisActiveAndEnabled: boolget_enabled_Injected(_unity_self: IntPtr): boolset_enabled_Injected(_unity_self: IntPtr, value: bool): voidget_isActiveAndEnabled_Injected(_unity_self: IntPtr): boolMonoBehaviourm_CancellationTokenSource: CancellationTokenSource destroyCancellationToken: CancellationTokenuseGUILayout: booldidStart: booldidAwake: boolrunInEditMode: boolallowPrefabModeInPlayMode: boolMonoBehaviour()RaiseCancellation(): voidIsInvoking(): boolCancelInvoke(): voidInvoke(methodName: string, time: float): voidInvokeRepeating(methodName: string, time: float, repeatRate: float): voidCancelInvoke(methodName: string): voidIsInvoking(methodName: string): boolStartCoroutine(methodName: string): CoroutineStartCoroutine(methodName: string, value: object): CoroutineStartCoroutine(routine: IEnumerator): CoroutineStartCoroutine_Auto(routine: IEnumerator): Coroutine (Obsolete)StopCoroutine(routine: IEnumerator): voidStopCoroutine(routine: Coroutine): voidStopCoroutine(methodName: string): voidStopAllCoroutines(): voidprint(message: object): voidConstructorCheck(self: Object): voidInternal_CancelInvokeAll(self: MonoBehaviour): voidInternal_IsInvokingAll(self: MonoBehaviour): boolInvokeDelayed(self: MonoBehaviour, methodName: string, time: float, repeatRate: float): voidCancelInvoke(self: MonoBehaviour, methodName: string): voidIsInvoking(self: MonoBehaviour, methodName: string): boolIsObjectMonoBehaviour(obj: Object): boolStartCoroutineManaged(methodName: string, value: object): CoroutineStartCoroutineManaged2(enumerator: IEnumerator): CoroutineStopCoroutineManaged(routine: Coroutine): voidStopCoroutineFromEnumeratorManaged(routine: IEnumerator): voidGetScriptClassName(): stringOnCancellationTokenCreated(): voidStopCoroutine_Injected(_unity_self: IntPtr, ref methodName: ManagedSpanWrapper): voidStopAllCoroutines_Injected(_unity_self: IntPtr): voidget_useGUILayout_Injected(_unity_self: IntPtr): boolset_useGUILayout_Injected(_unity_self: IntPtr, value: bool): voidget_didStart_Injected(_unity_self: IntPtr): boolget_didAwake_Injected(_unity_self: IntPtr): boolget_runInEditMode_Injected(_unity_self: IntPtr): boolset_runInEditMode_Injected(_unity_self: IntPtr, value: bool): voidget_allowPrefabModeInPlayMode_Injected(_unity_self: IntPtr): boolInternal_CancelInvokeAll_Injected(self: IntPtr): voidInternal_IsInvokingAll_Injected(self: IntPtr): boolInvokeDelayed_Injected(self: IntPtr, ref methodName: ManagedSpanWrapper, time: float, repeatRate: float): voidCancelInvoke_Injected(self: IntPtr, ref methodName: ManagedSpanWrapper): voidIsInvoking_Injected(self: IntPtr, ref methodName: ManagedSpanWrapper): boolIsObjectMonoBehaviour_Injected(obj: IntPtr): boolStartCoroutineManaged_Injected(_unity_self: IntPtr, ref methodName: ManagedSpanWrapper, value: object): CoroutineStartCoroutineManaged2_Injected(_unity_self: IntPtr, enumerator: IEnumerator): CoroutineStopCoroutineManaged_Injected(_unity_self: IntPtr, routine: IntPtr): voidStopCoroutineFromEnumeratorManaged_Injected(_unity_self: IntPtr, routine: IEnumerator): voidGetScriptClassName_Injected(_unity_self: IntPtr, out ret: ManagedSpanWrapper): voidOnCancellationTokenCreated_Injected(_unity_self: IntPtr): void
07 September 2025