Encapsulation exposes operations with contracts while hiding representation details and controlling changes to state. In a text editor, a document object can privately store text and cursor position and expose
insert, delete and moveCursor. Clients cannot directly splice internal storage or set a cursor outside the document. The document's methods enforce its representation invariant, and the implementation can later replace a character array by a different text representation without changing the public editing interface. Merely declaring a field private is insufficient if an accessor returns the mutable representation itself.Class inheritance defines a subclass by extending another class's behavior, often with method overriding and subtyping. An editor might have an abstract
Command type with execute, and subclasses for insertion and deletion. The controller keeps commands through the common type and dynamic dispatch chooses each command's actual operation. Alternatively, a specialized document can extend a base document's documented behavior. An inheritance relationship used for substitution must preserve the superclass contract; borrowing its code alone does not justify exposing incompatible behavior. Commands exemplify the Command pattern without exposing document storage to the controller.The constructor stores a reference, not a copy:Now
val d = new Dimension(3, 4)
val f = new Frame(d)
d.width = 10f.area is still , but the product of its stored dimension fields is . Even without retaining d, a client can break the same invariant through f.dimension.height = 9, because the accessor exposes the same mutable object. This is representation exposure through reference aliasing. A val prevents reassigning the reference; it does not freeze the referenced object.Make defensive copies on both entry and exit, while keeping the supplied The private stored dimensions have no aliases exposed to the caller and are never changed. Mutating the constructor argument affects only the caller's object; mutating a returned
Dimension class unchanged:class Frame(d: Dimension) {
require(d != null)
private val saved = new Dimension(d.width, d.height)
private val savedArea = saved.width * saved.height
def area: Int = savedArea
def dimension: Dimension = new Dimension(saved.width, saved.height)
def isPortrait: Boolean = saved.height >= saved.width
}Dimension affects only a fresh copy. Consequently the stored area is calculated once and stays equal to the stored dimensions' product. This defensive copying is sufficient because Dimension contains only primitive fields; a more deeply nested mutable structure could require a deep copy or an immutable representation.The multiplication above deliberately retains the supplied class's
Int arithmetic and accessor type. If an exact mathematical area outside that range is required, compute once with saved.width.toLong * saved.height.toLong and expose Long instead; avoiding aliases and avoiding integer overflow are separate concerns.Compare areas before and after each completed setter call. Widen before multiplication so all products of two The history invariant is that
Int dimensions are represented exactly:class LoggedResizableFrame(h: Int, w: Int) extends ResizableFrame(h, w) {
private var increases = BigInt(0)
private def actualArea: Long = height.toLong * width.toLong
override def height_=(h: Int): Unit = {
val before = actualArea
super.height_=(h)
if (actualArea > before) increases += 1
}
override def width_=(w: Int): Unit = {
val before = actualArea
super.width_=(w)
if (actualArea > before) increases += 1
}
def getIncreases: BigInt = increases
}increases counts exactly the completed dimension updates whose new area strictly exceeds their immediately preceding area. The constructor's starting area is a baseline, not an increase. Each overriding setter invokes the superclass setter once and increments precisely when its comparison succeeds. Equal-area changes do not count. The comparison is on area, not merely on which dimension increased; this also handles zero or signed dimensions without adding an unstated input restriction. An arbitrary-precision integer avoids eventual counter overflow.One base-class implementation changes the private fields directly:The completed operation leaves the area unchanged. It calls no overriding setters, so the logging subclass's count stays unchanged and does not need an override. The invariant is about completed public updates, not transient internal states between the two assignments.
def flip(): Unit = {
val oldH = _h
_h = _w
_w = oldH
}A second implementation uses public setters:These setter calls use dynamic dispatch on a logging receiver. Starting at height two and width three, the first call increases the temporary area from six to nine and wrongly increments the count, while the final area returns to six. This version needs a subclass override. With the counter name from part (i), the following override discards the internal setter contributions:For this specified base implementation the two assignments complete normally and the final area equals the initial area, so restoring the saved count is correct. Another design could suppress logging during the compound operation. The difference between the two base implementations illustrates the fragile base class problem: an internal refactoring can change subclass behavior through newly introduced virtual calls.
def flip(): Unit = {
val oldH = height
val oldW = width
height = oldW
width = oldH
}override def flip(): Unit = {
val before = increases
super.flip()
increases = before
}Use object composition: a logging wrapper owns a private
ResizableFrame and delegates dimension operations to it, rather than being its subclass. The wrapper takes the before/after area around each public operation. Its own flip calls the component's whole flip once, compares only the completed states, and therefore records no increase. The component must be exclusively owned or defensively copied, so clients cannot bypass the wrapper's accounting.The component's internal calls cannot dynamically dispatch into the separate wrapper. This prevents the setter-based
flip implementation change from creating false log entries and reduces dependence on undocumented superclass internals. The wrapper can also choose a smaller interface. Costs include forwarding code and the loss of automatic ResizableFrame subtyping: code requiring that concrete superclass cannot necessarily accept the wrapper. A shared abstract frame interface can recover substitutability while keeping implementation reuse based on composition. Composition improves this dependency boundary; it does not remove the need for a clear mutation and ownership contract. Articles by others on the same topic
There are currently no matching articles.