Lecture 13: Adapter Pattern - System Integration

๐Ÿ”Œ Adapter Pattern

Adapting Systems with an Adapter

Game Programming - CSCI 3213

Spring 2026 - Lecture 13

Oklahoma City University

๐Ÿ“š Learning Objectives

  • Understand the Adapter design pattern
  • Learn the difference between Object and Class Adapters
  • Recognize when to use Adapter vs Facade patterns
  • Implement adapters for third-party inventory systems
  • Avoid merge conflicts when updating vendor code
  • Create flexible, maintainable integration layers

๐ŸŒ Real-World Analogy

Power Adapters & Cable Converters

Think about traveling internationally with your phone:

  • Your Phone: Expects US-style plug (Client)
  • European Outlet: Provides European socket (Adaptee)
  • Travel Adapter: Converts between them (Adapter)

Key Properties:

  • Adapter doesn't modify the phone or the outlet
  • Phone doesn't know it's using an adapter
  • Outlet doesn't know an adapter is involved
  • You can swap adapters without changing either end
The Pattern: Bridge incompatible interfaces without modifying either side

๐Ÿ” Understanding the Adapter Pattern

Core Concept

Convert the interface of a class into another interface clients expect, allowing incompatible classes to work together.

When You Need Adapters:

  • Integrating third-party libraries with different APIs
  • Using legacy code with modern systems
  • Working with vendor code you can't modify
  • Avoiding merge conflicts during library updates
  • Creating a consistent interface across different implementations

The Problem It Solves

You have two systems that need to work together, but their interfaces are incompatible. You can't modify one or both systems directly.

โš–๏ธ Two Adapter Approaches

1. Object Adapter (Composition)

  • Uses composition - "has-a" relationship
  • Adapter wraps an instance of the Adaptee
  • More flexible - can adapt entire class hierarchies
  • Preferred in most scenarios

2. Class Adapter (Inheritance)

  • Uses inheritance - "is-a" relationship
  • Adapter inherits from the Adaptee
  • More direct but less flexible
  • Can override Adaptee behavior if needed
This Lecture: We'll focus on Class Adapter using inheritance, as it's more challenging to understand. Once you grasp this, Object Adapter is straightforward.

๐Ÿ—๏ธ Adapter Pattern Structure

Key Participants:

  • Client: Uses the Target interface
    • Your game code that needs inventory features
  • Target (Interface): Expected interface
    • IInventorySystem
  • Adapter: Bridges Target and Adaptee
    • InventorySystemAdapter
  • Adaptee: Existing incompatible class
    • InventorySystem (third-party)

Data Flow:

Client โ†’ Target Interface โ†’ Adapter โ†’ Adaptee

Client calls methods on the interface, Adapter translates those calls to the Adaptee's methods.

Pattern Type: Structural - focuses on composing classes and objects

๐Ÿค” Adapter vs Facade Pattern

Aspect Adapter Pattern Facade Pattern
Intent Convert interface to match client expectations Simplify complex subsystem
Number of Classes Usually adapts one class Wraps multiple classes
Interface Matches existing target interface Creates new simplified interface
Goal Compatibility between systems Ease of use
Remember: Adapter makes incompatible interfaces work together. Facade makes complex interfaces easier to use.

โœ… Benefits of Adapter Pattern

  • Non-Invasive Integration:
    • No modifications to existing code
    • Vendor code stays pristine
    • Easy to pull library updates
  • Reusability and Flexibility:
    • Continue using legacy code with new systems
    • Swap implementations without changing client code
    • Immediate return on investment
  • Single Responsibility:
    • Separates interface conversion from business logic
    • Each class has one reason to change
  • Open/Closed Principle:
    • Add new adapters without modifying existing code
    • Extend functionality through composition

โš ๏ธ Potential Drawbacks

  • Persisting Legacy Code:
    • Old code might limit upgrade options
    • Can become deprecated over time
    • May conflict with new Unity versions
  • Performance Overhead:
    • Additional layer of indirection
    • Extra method calls for adaptation
    • Usually negligible, but worth noting
  • Complexity:
    • More classes to manage
    • Can be confusing if overused
    • Need clear documentation
Mitigation: Plan eventual migration away from legacy systems. Use adapters as a temporary bridge, not a permanent crutch.

๐ŸŽฏ When to Use Adapter Pattern

Use Adapter When:

  • โœ… Integrating third-party libraries from Unity Asset Store
  • โœ… Working with legacy code you can't refactor
  • โœ… Adding features to vendor code without modification
  • โœ… Avoiding merge conflicts on library updates
  • โœ… Multiple systems need the same interface

Don't Use Adapter When:

  • โŒ You can modify the source code directly
  • โŒ The interface mismatch is simple (just wrap it)
  • โŒ You're trying to hide bad design (refactor instead)

Common Use Cases:

  • Cloud save + local save integration
  • Multiple analytics providers with unified interface
  • Cross-platform input systems

๐Ÿ“– Use Case: Inventory System

The Problem:

You've downloaded an excellent inventory system from the Unity Asset Store. It saves player items to a secure cloud backend, but it only supports cloud saves.

Your Requirements:

  • Need both cloud AND local disk saves for redundancy
  • Want to sync between local and cloud inventories
  • Can't modify vendor code (merge hell on updates)

The Solution:

Use the Adapter pattern to create InventorySystemAdapter that:

  • Inherits from vendor's InventorySystem
  • Adds local save functionality
  • Provides sync methods
  • Exposes consistent IInventorySystem interface

๐Ÿ’ป Third-Party InventorySystem

using UnityEngine;
using System.Collections.Generic;

namespace Chapter.Adapter
{
    // This is the third-party class we CANNOT modify
    public class InventorySystem
    {
        public void AddItem(InventoryItem item)
        {
            Debug.Log("Adding item to the cloud");
        }

        public void RemoveItem(InventoryItem item)
        {
            Debug.Log("Removing item from the cloud");
        }

        public List<InventoryItem> GetInventory()
        {
            Debug.Log(
                "Returning inventory list stored in the cloud");
            return new List<InventoryItem>();
        }
    }
}
Problem: No local save support, can't add it without modifying vendor code

๐Ÿ’ป SaveLocation Enum

namespace Chapter.Adapter
{
    public enum SaveLocation
    {
        Local,   // Save to local disk
        Cloud,   // Save to cloud backend
        Both     // Save to both locations
    }
}

Why an Enum?

  • Provides clear options to client code
  • Type-safe selection of save destinations
  • Easy to extend with new locations (e.g., RemoteServer)
  • Self-documenting code

๐Ÿ’ป IInventorySystem Interface

using System.Collections.Generic;

namespace Chapter.Adapter
{
    // This is the interface our client expects
    public interface IInventorySystem
    {
        void SyncInventories();

        void AddItem(
            InventoryItem item, SaveLocation location);

        void RemoveItem(
            InventoryItem item, SaveLocation location);

        List<InventoryItem> GetInventory(
            SaveLocation location);
    }
}

Key Differences from Vendor Interface:

  • Methods accept SaveLocation parameter
  • Adds SyncInventories() method
  • Client can specify where to save/load

๐Ÿ’ป InventorySystemAdapter (1/3)

using UnityEngine;
using System.Collections.Generic;

namespace Chapter.Adapter
{
    // Inherits from InventorySystem AND implements IInventorySystem
    public class InventorySystemAdapter :
        InventorySystem, IInventorySystem
    {
        private List<InventoryItem> _cloudInventory;

        // NEW functionality: Sync local and cloud
        public void SyncInventories()
        {
            var _cloudInventory = GetInventory();
            Debug.Log(
                "Synchronizing local drive and cloud inventories");
        }
Notice: We inherit from the Adaptee AND implement the Target interface. This is the Class Adapter approach.

๐Ÿ’ป InventorySystemAdapter (2/3)

        // ADAPTED method: AddItem with location choice
        public void AddItem(
            InventoryItem item, SaveLocation location)
        {
            if (location == SaveLocation.Cloud)
                AddItem(item); // Calls parent method!

            if (location == SaveLocation.Local)
                Debug.Log("Adding item to local drive");

            if (location == SaveLocation.Both)
                Debug.Log(
                    "Adding item to local drive and cloud");
        }

The Adaptation Magic:

  • AddItem(item) calls the inherited method
  • Local save logic is added here
  • Client gets unified interface for both

๐Ÿ’ป InventorySystemAdapter (3/3)

        public void RemoveItem(
            InventoryItem item, SaveLocation location)
        {
            Debug.Log(
                "Remove item from local/cloud/both");
        }

        public List<InventoryItem> GetInventory(
            SaveLocation location)
        {
            Debug.Log(
                "Get inventory from local/cloud/both");
            return new List<InventoryItem>();
        }
    }
}
Result: We've added local save support without touching the vendor's InventorySystem class!

๐Ÿ’ป InventoryItem Class

using UnityEngine;

namespace Chapter.Adapter
{
    [CreateAssetMenu(
        fileName = "New Item",
        menuName = "Inventory")]
    public class InventoryItem : ScriptableObject
    {
        // Placeholder class for inventory items
        // In production, add:
        // - Item name, description, icon
        // - Stack size, rarity, value
        // - Equipment type, stats, etc.
    }
}

ScriptableObject Benefits:

  • Designer-friendly asset creation
  • Shareable across scenes
  • No runtime instantiation overhead

๐Ÿ’ป ClientAdapter - Setup

using UnityEngine;

namespace Chapter.Adapter
{
    public class ClientAdapter : MonoBehaviour
    {
        public InventoryItem item;

        private InventorySystem _inventorySystem;
        private IInventorySystem _inventorySystemAdapter;

        void Start()
        {
            // Old vendor system
            _inventorySystem = new InventorySystem();

            // Our adapted system
            _inventorySystemAdapter =
                new InventorySystemAdapter();
        }
Key: Client can use either the old system OR the adapted system

๐Ÿ’ป ClientAdapter - Testing Interface

        void OnGUI()
        {
            if (GUILayout.Button("Add item (no adapter)"))
                _inventorySystem.AddItem(item);

            if (GUILayout.Button("Add item (with adapter)"))
                _inventorySystemAdapter.AddItem(
                    item, SaveLocation.Both);
        }
    }
}

What This Demonstrates:

  • First button: Uses old cloud-only system
  • Second button: Uses adapter with local+cloud saves
  • Client decides which to use
  • Both options available simultaneously

๐Ÿงช Testing the Implementation

Steps:

  1. Create new Unity scene
  2. Create InventoryItem ScriptableObject asset:
    • Assets โ†’ Create โ†’ Inventory โ†’ New Item
    • Name it "Test Sword" or similar
  3. Create empty GameObject
  4. Attach ClientAdapter script
  5. Drag Test Sword asset into the Item field
  6. Press Play
  7. Click both buttons and observe console logs
Expected Output: First button logs cloud save only. Second button logs both local and cloud saves.

๐Ÿ”„ How the Adapter Works

Call Flow Breakdown:

  1. Client calls:
    _inventorySystemAdapter.AddItem(item, SaveLocation.Both);
  2. Adapter receives call through IInventorySystem interface
  3. Adapter checks location:
    • If Cloud: calls AddItem(item) (inherited method)
    • If Local: executes local save logic
    • If Both: executes both branches
  4. Inherited method executes vendor's cloud save
  5. Result: Item saved to both locations
Key Insight: Adapter acts as a translator between client expectations and vendor implementation

๐Ÿ—๏ธ Class Adapter Inheritance

The Inheritance Chain:

    InventorySystem (Adaptee - vendor code)
            โ†‘
            | inherits
            |
    InventorySystemAdapter
            |
            | implements
            โ†“
    IInventorySystem (Target - what client expects)
                    

What This Gives Us:

  • Access to parent methods: Can call vendor's AddItem(), RemoveItem(), GetInventory()
  • Interface compliance: Adapter matches IInventorySystem contract
  • Additional features: SyncInventories(), location-aware methods
  • No vendor modification: InventorySystem stays pristine

๐Ÿ”„ Object Adapter Approach

Alternative Implementation (Composition):

public class InventorySystemAdapter : IInventorySystem
{
    // Wrap instead of inherit!
    private InventorySystem _inventorySystem;

    public InventorySystemAdapter()
    {
        _inventorySystem = new InventorySystem();
    }

    public void AddItem(InventoryItem item, SaveLocation location)
    {
        if (location == SaveLocation.Cloud)
            _inventorySystem.AddItem(item); // Delegate call

        if (location == SaveLocation.Local)
            Debug.Log("Adding item to local drive");

        // ... rest of implementation
    }
}
Object Adapter: More flexible, can adapt multiple classes, no inheritance constraints

๐Ÿค” Class vs Object Adapter

Use Class Adapter When:

  • โœ… You need to override Adaptee methods
  • โœ… Single inheritance is acceptable (C# supports one parent class)
  • โœ… Adaptee has protected members you need to access

Use Object Adapter When:

  • โœ… You need to adapt multiple classes
  • โœ… You want more flexibility (can swap Adaptee at runtime)
  • โœ… Inheritance hierarchy is already complex
  • โœ… You want to adapt an entire class hierarchy
General Recommendation: Prefer Object Adapter (composition over inheritance) unless you have a specific reason to inherit

๐Ÿ’พ Implementing Real Save Logic

using System.IO;
using UnityEngine;

public void AddItem(InventoryItem item, SaveLocation location)
{
    if (location == SaveLocation.Cloud || location == SaveLocation.Both)
    {
        AddItem(item); // Cloud save via parent
    }

    if (location == SaveLocation.Local || location == SaveLocation.Both)
    {
        // Real local save implementation
        string path = Application.persistentDataPath + "/inventory.json";
        var inventory = LoadLocalInventory(path);
        inventory.Add(item);
        string json = JsonUtility.ToJson(new InventoryList(inventory));
        File.WriteAllText(path, json);
        Debug.Log($"Saved {item.name} to {path}");
    }
}
Production Tip: Use JsonUtility or JSON.NET for serialization, handle exceptions, validate data

๐Ÿ”„ Implementing SyncInventories

public void SyncInventories()
{
    // Get inventories from both sources
    List<InventoryItem> cloudInventory = GetInventory();
    List<InventoryItem> localInventory = LoadLocalInventory();

    // Merge strategy: Cloud takes precedence
    var merged = new HashSet<InventoryItem>(cloudInventory);

    foreach (var item in localInventory)
    {
        if (!merged.Contains(item))
        {
            // Item exists locally but not in cloud - upload it
            AddItem(item, SaveLocation.Cloud);
            merged.Add(item);
        }
    }

    // Save merged inventory locally
    SaveLocalInventory(merged.ToList());

    Debug.Log($"Synced {merged.Count} items between local and cloud");
}

โš ๏ธ Error Handling Best Practices

public void AddItem(InventoryItem item, SaveLocation location)
{
    if (item == null)
    {
        Debug.LogError("Cannot add null item to inventory");
        return;
    }

    try
    {
        if (location == SaveLocation.Cloud || location == SaveLocation.Both)
        {
            AddItem(item);
        }

        if (location == SaveLocation.Local || location == SaveLocation.Both)
        {
            SaveToLocalDisk(item);
        }
    }
    catch (System.Exception e)
    {
        Debug.LogError($"Failed to add item: {e.Message}");
        // Implement rollback if needed
    }
}

๐Ÿ”Œ Multiple Adapters Pattern

Adapting Multiple Vendors:

// Adapter for Vendor A's system
public class VendorAInventoryAdapter : IInventorySystem
{
    private VendorAInventory _vendorA;
    // ... implement IInventorySystem
}

// Adapter for Vendor B's system
public class VendorBInventoryAdapter : IInventorySystem
{
    private VendorBInventory _vendorB;
    // ... implement IInventorySystem
}

// Client uses either adapter through same interface
IInventorySystem inventory;
if (useVendorA)
    inventory = new VendorAInventoryAdapter();
else
    inventory = new VendorBInventoryAdapter();

inventory.AddItem(item, SaveLocation.Both); // Works with both!
Benefit: Swap vendors without changing client code!

๐Ÿงช Unit Testing Adapters

using NUnit.Framework;

[Test]
public void Adapter_AddsItemToCloud_WhenCloudLocationSpecified()
{
    // Arrange
    var adapter = new InventorySystemAdapter();
    var item = ScriptableObject.CreateInstance<InventoryItem>();

    // Act
    adapter.AddItem(item, SaveLocation.Cloud);

    // Assert
    var inventory = adapter.GetInventory(SaveLocation.Cloud);
    Assert.Contains(item, inventory);
}

[Test]
public void Adapter_SyncsInventories_WithoutDuplicates()
{
    // Arrange
    var adapter = new InventorySystemAdapter();
    var item = ScriptableObject.CreateInstance<InventoryItem>();
    adapter.AddItem(item, SaveLocation.Both);

    // Act
    adapter.SyncInventories();

    // Assert
    var cloud = adapter.GetInventory(SaveLocation.Cloud);
    var local = adapter.GetInventory(SaveLocation.Local);
    Assert.AreEqual(cloud.Count, local.Count);
}

๐Ÿš€ Migrating Away from Vendor Code

Adapter as Temporary Bridge:

Phase 1: Integrate with Adapter

  • Use adapter to work with vendor code
  • All client code uses IInventorySystem interface

Phase 2: Build Your Own System

  • Create new InventorySystemV2 that implements IInventorySystem
  • Doesn't depend on vendor code
  • Has all features you need

Phase 3: Swap Implementation

// Change ONE line:
// IInventorySystem inventory = new InventorySystemAdapter();
IInventorySystem inventory = new InventorySystemV2();

// All client code continues working!

Phase 4: Remove Vendor Dependency

  • Delete adapter class
  • Remove vendor package

๐Ÿ›’ Real-World Unity Use Cases

Common Asset Store Scenarios:

  • Analytics Providers:
    • Unity Analytics, GameAnalytics, Firebase
    • Create IAnalyticsService interface
    • Adapt each provider to unified interface
    • Swap providers without touching game code
  • IAP Systems:
    • Unity IAP, Steam, console-specific stores
    • Adapt platform-specific APIs
    • Single IPurchaseService interface
  • Social Features:
    • PlayFab, GameSparks, custom backend
    • Unified ISocialService
    • Easy backend migration
  • Localization:
    • I2 Localization, Unity Localization Package
    • ILocalizationService adapter

โšก Performance Tips

Minimizing Overhead:

  • Avoid Deep Nesting:
    • Don't create adapters for adapters
    • Keep indirection to minimum
  • Cache Expensive Operations:
    private List<InventoryItem> _cachedInventory;
    private float _lastCacheTime;
    
    public List<InventoryItem> GetInventory(SaveLocation location)
    {
        if (Time.time - _lastCacheTime < 1f)
            return _cachedInventory; // Use cache
    
        _cachedInventory = LoadInventoryFromLocation(location);
        _lastCacheTime = Time.time;
        return _cachedInventory;
    }
  • Async Operations:
    • Don't block main thread with sync operations
    • Use async/await for cloud operations

๐Ÿšซ Common Mistakes to Avoid

  1. Modifying Vendor Code Directly:
    • โŒ Defeats the purpose of the adapter
    • โœ… Keep vendor code pristine, adapt it instead
  2. Not Using Interfaces:
    • โŒ Client depends on concrete adapter class
    • โœ… Client depends on IInventorySystem interface
  3. Over-Engineering:
    • โŒ Creating adapters for simple wrappers
    • โœ… Use adapter only when there's real incompatibility
  4. Ignoring Updates:
    • โŒ Never updating vendor library
    • โœ… Regularly pull updates, test adapter still works
  5. Leaky Abstractions:
    • โŒ Exposing vendor-specific details in interface
    • โœ… Keep interface vendor-agnostic

๐Ÿ› Debugging Adapter Issues

Add Comprehensive Logging:

public void AddItem(InventoryItem item, SaveLocation location)
{
    Debug.Log($"[Adapter] AddItem called: {item.name}, " +
              $"Location: {location}");

    if (location == SaveLocation.Cloud || location == SaveLocation.Both)
    {
        Debug.Log("[Adapter] Calling vendor's AddItem for cloud save");
        AddItem(item);
    }

    if (location == SaveLocation.Local || location == SaveLocation.Both)
    {
        Debug.Log("[Adapter] Executing local save logic");
        SaveToLocalDisk(item);
    }

    Debug.Log("[Adapter] AddItem completed successfully");
}

Verify Adapter is Used:

Debug.Assert(inventory is IInventorySystem,
    "Inventory must implement IInventorySystem interface!");

๐ŸŽฎ Real-World Industry Examples

Adapter Pattern in Production:

  • Cross-Platform Games:
    • Adapting platform-specific APIs (Xbox, PlayStation, Switch)
    • Unified save system across platforms
  • Engine Migrations:
    • Moving from Unreal to Unity (or vice versa)
    • Adapter layer maintains API compatibility
    • Gradual migration of systems
  • Legacy System Integration:
    • Modern UI talking to 10-year-old backend
    • Adapter translates between protocols
  • Third-Party SDKs:
    • Adapting vendor SDKs for achievements, leaderboards
    • Prevents vendor lock-in
Common Theme: Adapter enables integration without modification

๐Ÿ“ Summary

What We Learned:

  • โœ… Adapter pattern converts incompatible interfaces
  • โœ… Two approaches: Object Adapter (composition) and Class Adapter (inheritance)
  • โœ… Enables use of vendor code without modification
  • โœ… Avoids merge conflicts when updating libraries
  • โœ… Different from Facade (compatibility vs simplification)
  • โœ… Useful for Asset Store integrations and legacy systems

Key Takeaways:

  • Adapter sits between client and incompatible system
  • Client uses target interface, adapter translates to adaptee
  • Prefer composition (Object Adapter) over inheritance
  • Use adapters as temporary bridges, plan for migration
  • Common in cross-platform and vendor integration scenarios

๐Ÿ’ช Practice Exercise

Build a Multi-Platform Save System

Requirements:

  1. Create ISaveSystem interface with methods:
    • Save(string key, string data)
    • Load(string key)
    • Delete(string key)
  2. Implement adapters for:
    • PlayerPrefs (Unity's built-in system)
    • File system (JSON files)
    • Cloud save (mock with Debug.Log)
  3. Create client code that:
    • Saves player data to all three systems
    • Loads from fallback chain (cloud โ†’ file โ†’ PlayerPrefs)
    • Syncs between systems
Bonus: Add encryption/decryption to file system adapter

๐Ÿ“š Additional Resources

Further Reading:

  • Design Patterns (GoF): Original Adapter pattern definition
  • Game Development Patterns with Unity 2021:
    • Chapter 14: Adapting Systems with an Adapter
  • Head First Design Patterns: Adapter chapter with great examples
  • Refactoring.Guru:
    • https://refactoring.guru/design-patterns/adapter

Unity Resources:

  • Unity Asset Store: Best practices for integrating third-party assets
  • Unity Manual: Serialization and save systems

Next Lecture:

Lecture 14: Game Architecture & Systems Design

โ“ Questions?

Common Questions:

  • Q: When should I use Adapter vs Facade?
    • A: Adapter for interface compatibility, Facade for simplifying complex subsystems
  • Q: Can I modify the adapter later?
    • A: Yes! That's the point - adapter is YOUR code, modify freely
  • Q: What if vendor changes their API?
    • A: Update adapter to match new API, client code stays the same
  • Q: Should every third-party library get an adapter?
    • A: Only if you can't modify it and need different interface, or want to prevent vendor lock-in
  • Q: Performance cost of adapters?
    • A: Minimal - one extra method call. Usually negligible.

Thank you! ๐ŸŽฎ