Files
docs-rust/ch18/ch18-01-what-is-oo.html
2026-06-22 21:27:36 +05:30

166 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Characteristics of Object-Oriented Languages</title>
</head>
<body>
<h2 id="characteristics-of-object-oriented-languages"><a class="header" href="#characteristics-of-object-oriented-languages">Characteristics of Object-Oriented Languages</a></h2>
<p>There is no consensus in the programming community about what features a
language must have to be considered object oriented. Rust is influenced by many
programming paradigms, including OOP; for example, we explored the features
that came from functional programming in Chapter 13. Arguably, OOP languages
share certain common characteristics—namely, objects, encapsulation, and
inheritance. Lets look at what each of those characteristics means and whether
Rust supports it.</p>
<h3 id="objects-contain-data-and-behavior"><a class="header" href="#objects-contain-data-and-behavior">Objects Contain Data and Behavior</a></h3>
<p>The book <em>Design Patterns: Elements of Reusable Object-Oriented Software</em> by
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley,
1994), colloquially referred to as <em>The Gang of Four</em> book, is a catalog of
object-oriented design patterns. It defines OOP in this way:</p>
<blockquote>
<p>Object-oriented programs are made up of objects. An <strong>object</strong> packages both
data and the procedures that operate on that data. The procedures are
typically called <strong>methods</strong> or <strong>operations</strong>.</p>
</blockquote>
<p>Using this definition, Rust is object oriented: Structs and enums have data,
and <code>impl</code> blocks provide methods on structs and enums. Even though structs and
enums with methods arent <em>called</em> objects, they provide the same
functionality, according to the Gang of Fours definition of objects.</p>
<h3 id="encapsulation-that-hides-implementation-details"><a class="header" href="#encapsulation-that-hides-implementation-details">Encapsulation That Hides Implementation Details</a></h3>
<p>Another aspect commonly associated with OOP is the idea of <em>encapsulation</em>,
which means that the implementation details of an object arent accessible to
code using that object. Therefore, the only way to interact with an object is
through its public API; code using the object shouldnt be able to reach into
the objects internals and change data or behavior directly. This enables the
programmer to change and refactor an objects internals without needing to
change the code that uses the object.</p>
<p>We discussed how to control encapsulation in Chapter 7: We can use the <code>pub</code>
keyword to decide which modules, types, functions, and methods in our code
should be public, and by default everything else is private. For example, we
can define a struct <code>AveragedCollection</code> that has a field containing a vector
of <code>i32</code> values. The struct can also have a field that contains the average of
the values in the vector, meaning the average doesnt have to be computed on
demand whenever anyone needs it. In other words, <code>AveragedCollection</code> will
cache the calculated average for us. Listing 18-1 has the definition of the
<code>AveragedCollection</code> struct.</p>
<figure class="listing" id="listing-18-1">
<span class="file-name">Filename: src/lib.rs</span>
<pre><code class="language-rust noplayground">pub struct AveragedCollection {
list: Vec&lt;i32&gt;,
average: f64,
}</code></pre>
<figcaption><a href="#listing-18-1">Listing 18-1</a>: An <code>AveragedCollection</code> struct that maintains a list of integers and the average of the items in the collection</figcaption>
</figure>
<p>The struct is marked <code>pub</code> so that other code can use it, but the fields within
the struct remain private. This is important in this case because we want to
ensure that whenever a value is added or removed from the list, the average is
also updated. We do this by implementing <code>add</code>, <code>remove</code>, and <code>average</code> methods
on the struct, as shown in Listing 18-2.</p>
<figure class="listing" id="listing-18-2">
<span class="file-name">Filename: src/lib.rs</span>
<pre><code class="language-rust noplayground"><span class="boring">pub struct AveragedCollection {
</span><span class="boring"> list: Vec&lt;i32&gt;,
</span><span class="boring"> average: f64,
</span><span class="boring">}
</span><span class="boring">
</span>impl AveragedCollection {
pub fn add(&amp;mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&amp;mut self) -&gt; Option&lt;i32&gt; {
let result = self.list.pop();
match result {
Some(value) =&gt; {
self.update_average();
Some(value)
}
None =&gt; None,
}
}
pub fn average(&amp;self) -&gt; f64 {
self.average
}
fn update_average(&amp;mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}</code></pre>
<figcaption><a href="#listing-18-2">Listing 18-2</a>: Implementations of the public methods <code>add</code>, <code>remove</code>, and <code>average</code> on <code>AveragedCollection</code></figcaption>
</figure>
<p>The public methods <code>add</code>, <code>remove</code>, and <code>average</code> are the only ways to access
or modify data in an instance of <code>AveragedCollection</code>. When an item is added to
<code>list</code> using the <code>add</code> method or removed using the <code>remove</code> method, the
implementations of each call the private <code>update_average</code> method that handles
updating the <code>average</code> field as well.</p>
<p>We leave the <code>list</code> and <code>average</code> fields private so that there is no way for
external code to add or remove items to or from the <code>list</code> field directly;
otherwise, the <code>average</code> field might become out of sync when the <code>list</code>
changes. The <code>average</code> method returns the value in the <code>average</code> field,
allowing external code to read the <code>average</code> but not modify it.</p>
<p>Because weve encapsulated the implementation details of the struct
<code>AveragedCollection</code>, we can easily change aspects, such as the data structure,
in the future. For instance, we could use a <code>HashSet&lt;i32&gt;</code> instead of a
<code>Vec&lt;i32&gt;</code> for the <code>list</code> field. As long as the signatures of the <code>add</code>,
<code>remove</code>, and <code>average</code> public methods stayed the same, code using
<code>AveragedCollection</code> wouldnt need to change. If we made <code>list</code> public instead,
this wouldnt necessarily be the case: <code>HashSet&lt;i32&gt;</code> and <code>Vec&lt;i32&gt;</code> have
different methods for adding and removing items, so the external code would
likely have to change if it were modifying <code>list</code> directly.</p>
<p>If encapsulation is a required aspect for a language to be considered object
oriented, then Rust meets that requirement. The option to use <code>pub</code> or not for
different parts of code enables encapsulation of implementation details.</p>
<h3 id="inheritance-as-a-type-system-and-as-code-sharing"><a class="header" href="#inheritance-as-a-type-system-and-as-code-sharing">Inheritance as a Type System and as Code Sharing</a></h3>
<p><em>Inheritance</em> is a mechanism whereby an object can inherit elements from
another objects definition, thus gaining the parent objects data and behavior
without you having to define them again.</p>
<p>If a language must have inheritance to be object oriented, then Rust is not
such a language. There is no way to define a struct that inherits the parent
structs fields and method implementations without using a macro.</p>
<p>However, if youre used to having inheritance in your programming toolbox, you
can use other solutions in Rust, depending on your reason for reaching for
inheritance in the first place.</p>
<p>You would choose inheritance for two main reasons. One is for reuse of code:
You can implement particular behavior for one type, and inheritance enables you
to reuse that implementation for a different type. You can do this in a limited
way in Rust code using default trait method implementations, which you saw in
Listing 10-14 when we added a default implementation of the <code>summarize</code> method
on the <code>Summary</code> trait. Any type implementing the <code>Summary</code> trait would have
the <code>summarize</code> method available on it without any further code. This is
similar to a parent class having an implementation of a method and an
inheriting child class also having the implementation of the method. We can
also override the default implementation of the <code>summarize</code> method when we
implement the <code>Summary</code> trait, which is similar to a child class overriding the
implementation of a method inherited from a parent class.</p>
<p>The other reason to use inheritance relates to the type system: to enable a
child type to be used in the same places as the parent type. This is also
called <em>polymorphism</em>, which means that you can substitute multiple objects for
each other at runtime if they share certain characteristics.</p>
<section class="note" aria-role="note">
<h3 id="polymorphism"><a class="header" href="#polymorphism">Polymorphism</a></h3>
<p>To many people, polymorphism is synonymous with inheritance. But its
actually a more general concept that refers to code that can work with data of
multiple types. For inheritance, those types are generally subclasses.</p>
<p>Rust instead uses generics to abstract over different possible types and
trait bounds to impose constraints on what those types must provide. This is
sometimes called <em>bounded parametric polymorphism</em>.</p>
</section>
<p>Rust has chosen a different set of trade-offs by not offering inheritance.
Inheritance is often at risk of sharing more code than necessary. Subclasses
shouldnt always share all characteristics of their parent class but will do so
with inheritance. This can make a programs design less flexible. It also
introduces the possibility of calling methods on subclasses that dont make
sense or that cause errors because the methods dont apply to the subclass. In
addition, some languages will only allow <em>single inheritance</em> (meaning a
subclass can only inherit from one class), further restricting the flexibility
of a programs design.</p>
<p>For these reasons, Rust takes the different approach of using trait objects
instead of inheritance to achieve polymorphism at runtime. Lets look at how
trait objects work.</p>
</body>
</html>