IID.systems
ProfileServicesFormal MethodsAI AlignmentEssaysBookSchoolGitHub日本語
日本語

Interfaces 101: Separating Controls from Machinery

Understand the promise visible from the outside through a car's steering wheel and pedals and a television remote, then connect it to encapsulation, replaceable implementations, and polymorphism

Interfaces in everyday life

To drive a car, a driver turns the steering wheel and presses the accelerator or brake. The driver does not directly operate the steering linkage, fuel injection, motors, or hydraulics. Even when the model or power source changes, the basic controls presented to the driver remain familiar.

A television remote works the same way. The viewer uses buttons for power, volume, and channels. They can complete the operation without knowing how an infrared or radio signal is sent or how the television's circuits respond.

Car controls

The driver uses a common control scheme while different mechanisms inside each vehicle carry out those operations in their own way.

Steer

Stop

Go

Driver
Wheel and pedals
Vehicle mechanisms

Television remote

The button names and signal rules form an operational promise connecting the viewer and the television.

Power
Volume +
Channel
Viewer
Buttons and signals
Television circuits

An interface is a promise about how to operate something, placed at the boundary between a user and its internals. The user needs to know only that promise, not the internal mechanism, to achieve a goal. Here, interface does not mean only a screen; it means any point where people and machines or software components meet.


In software, model what can be done

A software component's user needs a promise about which operations can be called, which values are accepted, and which values are returned. A TypeScript interface expresses that promise as a type.

// Declare only the operations promised to the user interface CarControls { steer(direction: number): void accelerate(amount: number): void brake(amount: number): void } interface RemoteControllable { power(): void volumeUp(): void selectChannel(channel: number): void }

CarControls declares only that steer, accelerate, and brake are available. RemoteControllable declares only power, volume, and channel operations. Calling code can handle any object that fulfills the promise in the same way.

Crucially, these declarations do not prescribe how an engine, motor, infrared transmitter, or display must be implemented. Type compatibility checks the shape of an operation; it does not prove that an implementation produces the correct result.


Encapsulation: hide internals and protect valid state

If an interface is the promise shown on the outside, encapsulation keeps implementation details and state inside the boundary. Users cannot arbitrarily rewrite internal state; they work through the published operations.

class ElectricCar implements CarControls { #speed = 0 #batteryPercent = 80 steer(direction: number) { if (direction < -1 || direction > 1) { throw new RangeError('direction must be between -1 and 1') } // Operate the internal steering mechanism } accelerate(amount: number) { if (amount < 0 || amount > 1) { throw new RangeError('amount must be between 0 and 1') } if (this.#batteryPercent === 0) return this.#speed += amount * 5 this.#batteryPercent -= 1 } brake(amount: number) { if (amount < 0 || amount > 1) { throw new RangeError('amount must be between 0 and 1') } this.#speed = Math.max(0, this.#speed - amount * 8) } }

In this ElectricCar, speed and battery level are private fields. External code cannot change them directly and must use accelerate, which checks its input range, or brake, which prevents speed from falling below zero. Conditions the object must preserve can therefore be maintained in one place.

Encapsulation does not mean adding a getter and setter for every field. Exposing internal data directly makes callers depend on its structure. Publish operations that express intent, such as accelerate, instead of exposing raw assignments such as set speed.

An interface decides what to show outside; encapsulation decides how to hide and protect what is inside. They are distinct concepts, but they work together as two sides of the same boundary.


Polymorphism: same operation, different machinery

Polymorphism is the ability to treat multiple implementations of one interface through the same calls. Calling code programs against the shared promise instead of a concrete kind of object.

Driving code

CarControls

The same calls

GasolineCar: implemented by an engine

ElectricCar: implemented by a motor

In a gasoline car, the accelerator controls fuel and the engine; in an electric car, it controls power from the battery to a motor. Braking may use hydraulics or regeneration. If both vehicles fulfill CarControls, however, the driver-facing code can remain the same.

class GasolineCar implements CarControls { steer(direction: number) { /* Turn the front wheels */ } accelerate(amount: number) { /* Feed fuel and raise engine speed */ } brake(amount: number) { /* Apply hydraulic brakes */ } } class ElectricCar implements CarControls { steer(direction: number) { /* Steer with an electric motor */ } accelerate(amount: number) { /* Send power to the drive motor */ } brake(amount: number) { /* Use regenerative braking */ } } function startDriving(car: CarControls) { car.accelerate(0.4) car.steer(0.2) car.brake(0.3) } startDriving(new GasolineCar()) startDriving(new ElectricCar()) // The caller stays the same

The caller does not need an if or switch for every vehicle type. The object it receives performs its own implementation. A new vehicle can be added without changing startDriving as long as the shared promise remains intact.


Why this makes design easier

A small, explicit boundary lets users and implementers reason separately. Internals can change without forcing changes on the other side as long as the promise is preserved.

Creating an interface alone does not make a good design. Ambiguous names, huge lists of operations, and promises that implementations cannot keep create more coupling. Define the smallest set of operations users actually need, with clear meaning and constraints.

Replace implementations

Swap a gasoline car for an electric one, or real hardware for a simulator, while keeping the calling side when both fulfill the same promise.

Divide the work

Once a boundary is agreed, the calling and implementing sides can be built separately, and each team can limit what it needs to understand.

Test in isolation

Pass a fake with the same interface instead of a real car or television and verify the caller's behavior independently.


Common misconceptions

1

An interface is not only a screen. APIs, function inputs and outputs, public class operations, and signals between devices are interfaces between components.

2

Encapsulation is not merely adding private or generating getters and setters. It protects internal state and keeps users from depending on internal structure.

3

Polymorphism is not another name for an if or switch that inspects a type. Its core is calling through a shared promise and letting the concrete implementation choose its behavior.


Summary

1

An interface is a promise about operations available at a boundary. It states what can be done and separates users from how it is implemented.

2

Encapsulation locks implementation and state inside and preserves valid state through published operations. It is distinct from an interface but forms the other side of the same boundary.

3

Polymorphism lets different implementations of one interface be used by the same code, reducing type-specific branches in callers.

4

A small, clear promise makes replacement, division of work, and testing easier. Conforming to a type still does not guarantee that an implementation is correct.