In der realen Welt (und im Code) gibt es Entitäten, die irgendwo zwischen einem einfachen Skalar und einem vollwertigen Objekt liegen. Was bedeuten Dinge wie 80 Kilogramm, 25 Kilometer pro Stunde oder 25 × 25 × 50 cm eigentlich? Sie sind offensichtlich keine Skalare im engeren Sinne – sie lassen sich nicht auf einen einzelnen Wert wie 25 oder «Buenos Aires» reduzieren. Aber sie teilen auch ein zentrales Merkmal mit Skalaren: Sie existieren nicht unabhängig in der Realität. 80 Kilogramm beschreiben das Gewicht von etwas, genauso wie 25 km/h die Geschwindigkeit von etwas darstellen. Dimensionen beschreiben eine Kiste, ein Möbelstück oder ein Klavier – bedeuten aber für sich allein wenig.
    Deshalb lassen sich diese Entitäten schwer kategorisieren. Wenn wir sie im Code modellieren, ist es leicht, in unbeholfene oder unvollständige Darstellungen zu verfallen. Zum Glück bietet uns Rails ein leistungsstarkes Werkzeug: die Klassenmethode composed_of, mit der sich solche Werte präzise und ausdrucksstark modellieren lassen.
    Wann sollte man composed_of verwenden?
Es gibt zwei ziemlich intuitive Hinweise darauf, dass man composed_of verwenden sollte. Der erste ist, wenn du eine Gruppe von Model-Attributen bemerkst, die immer gemeinsam verwendet werden und im Grunde eine eigene konzeptuelle Einheit bilden. Zum Beispiel, wenn dein 
    Hotel-Modell die Attribute latitude und longitude besitzt und du diese immer als Einheit behandelst: eine geografische Koordinate.Ein gängiger, aber fehleranfälliger Ansatz:
    
  class Hotel < ApplicationRecord
  def coordinates = [latitude, longitude]
  def distance_to(other_hotel)
    lat1, lon1 = latitude, longitude
    lat2, lon2 = other_hotel.latitude, other_hotel.longitude
    rad_per_deg = Math::PI / 180
    r_km = 6371 # mittlerer Erdradius in km
    dlat_rad = (lat2 - lat1) * rad_per_deg
    dlon_rad = (lon2 - lon1) * rad_per_deg
    lat1_rad, lat2_rad = lat1 * rad_per_deg, lat2 * rad_per_deg
    a = Math.sin(dlat_rad / 2)**2 +
        Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2)**2
    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
    r_km * c
  end
end
    Das funktioniert, aber es fehlt an semantischer Klarheit. Es wird nicht explizit modelliert, dass latitude und longitude ein einziges Konzept bilden. Zudem werden hilfreiche Verhaltensweisen wie Validierung oder Distanzberechnung nicht gekapselt. Um das zu verbessern, könnte man zwei alternative Wege einschlagen – beide besser, aber nicht ideal.
    Ein neues Modell erstellen
Ein Ansatz besteht darin, ein separates 
    Coordinate-Modell zu erstellen, das dieses Verhalten kapselt. Damit lassen sich Validierungen leicht umsetzen, und es entsteht mehr Klarheit:
  class Coordinate < ApplicationRecord
  EARTH_RADIUS_KM = 6371
  belongs_to :locatable, polymorphic: true
  validates :latitude, presence: true,
            numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }
  validates :longitude, presence: true,
            numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }
  def distance_to(other_coordinate)
    rad_per_deg = Math::PI / 180
    dlat_rad = (other_coordinate.latitude - latitude) * rad_per_deg
    dlon_rad = (other_coordinate.longitude - longitude) * rad_per_deg
    lat1_rad = latitude * rad_per_deg
    lat2_rad = other_coordinate.latitude * rad_per_deg
    a = Math.sin(dlat_rad / 2)**2 +
        Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2)**2
    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
    EARTH_RADIUS_KM * c
  end
  def to_a = [latitude, longitude]
end
    Das ist eine deutliche Verbesserung, bringt aber zusätzliche Komplexität mit sich. Jedes Mal, wenn du ein 
    Hotel lädst, musst du auch den zugehörigen Koordinaten-Datensatz laden – sonst riskierst du N+1-Abfragen.Verwendung von Concerns
Eine weitere Möglichkeit besteht darin, ein Modul (ein 
    ActiveSupport::Concern) zu definieren, das die Coordinate-Logik direkt innerhalb des Hotel-Modells kapselt:
  module Geocodable
  extend ActiveSupport::Concern
  EARTH_RADIUS_KM = 6371
  included do
    validates :latitude, presence: true,
              numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }
    validates :longitude, presence: true,
              numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }
  end
  def coordinates = [latitude, longitude]
  def distance_to(other)
    ...
  end
end
    Das ist gegenüber einer separaten Tabelle ein Fortschritt. Es ermöglicht Validierungen und erlaubt dir, beliebige geokodierbare Objekte zu vergleichen – egal ob es sich um Hotels oder Freizeitparks handelt. Aber die Koordinaten bleiben zwei lose Attribute. Es gibt keine Unveränderlichkeit (Immutability), und nichts hindert dich daran, nur einen der beiden Werte zu aktualisieren – was keinen Sinn ergibt bei etwas, das einen einzigen Punkt im Raum darstellen soll.
    Dieser Ansatz leidet zudem unter Namensraum-Verschmutzung: Methoden wie 
    distance_to befinden sich direkt neben fachlichen Methoden wie hotel.name oder hotel.check_availability. Es wird schwieriger, zu erkennen, was zur Domäne des Hotels gehört und was zur Standortlogik. Und da die Methoden verstreut sind, geht das Gefühl verloren, dass die Koordinaten ein zusammenhängendes Konzept darstellen.Die elegante Lösung: composed_of
Zum Glück bietet uns Rails eine saubere und ausdrucksstarke Möglichkeit, Value Objects mit mehreren Attributen zu modellieren – ohne zusätzliche Tabellen oder Probleme mit dem Namensraum: composed_of.
    
  class Coordinate
  include ActiveModel::Validations
  attr_reader :latitude, :longitude
  EARTH_RADIUS_KM = 6371
  validates :latitude, presence: true,
            numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }
  validates :longitude, presence: true,
            numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }
  def initialize(latitude, longitude)
    @latitude = latitude
    @longitude = longitude
  end
  def distance_to(other)
    rad_per_deg = Math::PI / 180
    dlat_rad = (other.latitude - latitude) * rad_per_deg
    dlon_rad = (other.longitude - longitude) * rad_per_deg
    lat1_rad = latitude * rad_per_deg
    lat2_rad = other.latitude * rad_per_deg
    a = Math.sin(dlat_rad / 2)**2 +
        Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2)**2
    c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
    EARTH_RADIUS_KM * c
  end
end
class Hotel < ApplicationRecord
  composed_of :coordinates,
              class_name: 'Coordinate',
              mapping: [%w(latitude latitude), %w(longitude longitude)],
              allow_nil: true,
              converter: Proc.new { |value|
                case value
                when Coordinate
                  value
                when [latitude, longitude]
                  Coordinate.new(latitude, longitude)
                when { latitude:, longitude: }
                  Coordinate.new(latitude, longitude)
                else
                  raise ArgumentError, "Cannot convert #{value.inspect} to Coordinate"
                end
              }
  validate :coordinates_must_be_valid
  def coordinates_must_be_valid
    return unless coordinates.present?
    return if coordinates.valid?
    coordinates.errors.full_messages.each do |message|
      errors.add(:coordinates, message)
    end
  end
end
    Beispiele
  hotel1 = Hotel.create!(name: "Hilton", coordinates: [40.7128, -74.0060])
hotel2 = Hotel.create!(name: "Marriott", coordinates: { latitude: 40.7580, longitude: -73.9855 })
distance = hotel1.coordinates.distance_to(hotel2.coordinates) # => 7.3 km
    Vorteile
Dieser Ansatz hat mehrere Vorteile:
    - Klare Modellierung: Du machst explizit, dass latitude und longitude gemeinsam ein Konzept darstellen.
- Wiederverwendbarkeit: DasCoordinate-Objekt ist wiederverwendbar, testbar und kapselt Logik wie Distanzberechnung und Validierung.
- Validation: Du kannst Validierungen direkt in das Value Object integrieren, und Hotel kann überprüfen, ob seine Koordinaten gültig sind.
- Keine zusätzliche Datenbanktabelle nötig: Du bekommst alle Vorteile eines Objekts ohne die Komplexität einer eigenen Tabelle.
- Keine Namensraum-Verschmutzung: Standortlogik bleibt in derCoordinate-Klasse, Geschäftslogik im Modell.
Fazit
Wenn du zwei oder mehr Attribute hast, die gemeinsam ein Konzept beschreiben, aber keine eigene Datenbank-ID oder -Tabelle benötigen, ist composed_of ein eleganter Weg, sie in ein Value Object zu verpacken. Es macht deinen Code nicht nur ausdrucksstärker und wartbarer, sondern auch robuster gegenüber semantischen Fehlern.
 
     
     
     
     
     
     
    