Chapter 3. Abstraction and encapsulation

This chapter covers

  • Understanding the value of abstraction in large systems
  • Encapsulating related code into classes
  • Using encapsulation, inheritance, and composition in Python
  • Recognizing programming styles in Python

You’ve already seen that organizing your code into functions, classes, and modules is a great way to separate concerns, but you can also use these techniques to separate complexity in your code. Because it’s difficult to remember every detail about your software at all times, in this chapter you’ll learn to use abstraction and encapsulation to create levels of granularity in your code so you can worry about the details only when you need to.

3.1. What is abstraction?

When you hear the word abstract, what do you think of? Usually a Jackson Pollock painting or a Calder sculpture runs through my mind. Abstract art is marked by a freedom from concrete form, often only suggestive of a specific subject. Abstraction, then, would be the process of taking something concrete and stripping it of specifics. When speaking about abstraction in software, this is exactly right!

3.1.1. The “black box”

As you develop software, pieces of it will come to represent concepts in full. Once you’ve finished developing a particular function, for example, it can be used for its intended purpose over and over again without you having to think too hard about how it works. At this point, the function has become a black box. A black box is a calculation or behavior that “just works”—it doesn’t need to be opened up and examined each time you need it (see figure 3.1).

Figure 3.1. Treating working software as a black box

Suppose you’re building a natural-language processing system that determines if a product review is positive, negative, or neutral. Such a system has many steps along the way, as shown in figure 3.2:

  1. Break up the review into sentences.
  2. Break each sentence into words or phrases, generally called tokens.
  3. Simplify word variations to their root words, called lemmatization.
  4. Determine the grammatical structure of the sentence.
  5. Calculate the polarity of the content by comparing it to manually labeled training data.
  6. Calculate the overall magnitude of polarity.
  7. Choose a final positive, negative, or neutral determination for the product review.
Figure 3.2. Determining whether a product review is positive, negative, or neutral

Each step in the sentiment analysis workflow is composed of many lines of code. By rolling that code up into concepts like “break into sentences” and “determine grammatical structure,” the whole workflow becomes easier to follow than if you were trying to comprehend all the code at once. If someone wants to know the specifics of a particular step in the workflow, they can choose to take a deeper look. This idea of abstracting an implementation is useful for human comprehension, but it’s also something that can be formalized in code to produce more stable results.

In chapter 2, you learned how to identify the concerns of your code and extract them into functions. Abstracting a behavior into a function allows you to freely change how that function calculates a result, as long as the inputs and return data type stay the same. This means if you find a bug or a faster or more accurate way of performing the calculation, you can swap that behavior in without other code needing to change as a result. This gives you flexibility as you iterate on software.

3.1.2. Abstraction is like an onion

You saw in figure 3.2 that each step in a workflow generally represents some lower-level code. Some of those steps, though, such as determining the grammatical structure of a sentence, are quite involved. Complex code like this will often benefit from layers of abstraction; low-level utilities support small behaviors, which in turn support more involved behaviors. Because of this, writing and reading code in large systems is often like peeling an onion, revealing smaller, more tightly packed pieces of code underneath (figure 3.3).

Figure 3.3. Abstraction works in layers of complexity.

Small, focused behaviors that get used again and again sit in the lower layers and need to change infrequently. The big concepts, business logic, and complex moving parts show up as you go further out; they change more frequently because of changing requirements, but they still make use of the smaller behaviors.

When you’re starting out, it’s common to write one long, procedural program that gets a job done. This works fine when prototyping, but it reveals its poor maintainability when someone needs to read all 100 lines of code to figure out where they need to make a change or fix a bug. Introducing abstraction with features of the language makes it easier to pinpoint the relevant code. In Python, features like functions, classes, and modules help abstract behavior. Let’s see how using functions in Python helps with the first two steps of the sentiment-analysis workflow.

When working through the code in listing 3.1, you might notice that it does some similar work twice—the work of splitting a string up by sentence and by individual words in each sentence is quite similar. Each step performs the same operation, with different inputs. This is usually an opportunity to factor a behavior into its own function.

Listing 3.1. A procedure for splitting a paragraph into sentences and tokens
import re

product_review = '''This is a fine milk, but the product
line appears to be limited in available colors. I
could only find white.'''                                    1

sentence_pattern = re.compile(r'(.*?.)(s|$)', re.DOTALL)   2
matches = sentence_pattern.findall(product_review)           3
sentences = [match[0] for match in matches]                  4

word_pattern = re.compile(r"([w-']+)([s,.])?")            5
for sentence in sentences:
    matches = word_pattern.findall(sentence)
    words = [match[0] for match in matches]                  6
    print(words)

  • 1 The product review as a string
  • 2 Matches full sentences ending with a period
  • 3 Finds all sentences in the review
  • 4 findall returns list of (sentence, white space) pairs
  • 5 Matches single words
  • 6 For each sentence, gets all the words

You can see that the work to find the sentences and words is similar, with the pattern to match against being the distinguishing feature. Some logistics also have to be taken care of, like dealing with the output of findall, that clutter up the code. At a quick glance, the intent of this code might not be obvious.

Note

In real natural-language processing, splitting sentences and words is difficult, so difficult, in fact, that the software to parse them generally uses probabilistic modeling to determine the result. Probabilistic modeling uses a large body of input testing data to determine the likely correctness of a particular result. The result might not always be the same! Natural languages are complex, and it shows when we try to make computers understand them.

How can abstraction help improve the sentence parsing? With a little help from Python functions, you can simplify this a bit. In the following listing, the pattern-matching is abstracted into a get_matches_for_pattern function.

Listing 3.2. Refactored sentence parsing
import re

def get_matches_for_pattern(pattern, string):              1
    matches = pattern.findall(string)
    return [match[0] for match in matches]

product_review = '...'

sentence_pattern = re.compile(r'(.*?.)(s|$)', re.DOTALL)
sentences = get_matches_for_pattern(                       2
    sentence_pattern,
    product_review,
)

word_pattern = re.compile(r"([w-']+)([s,.])?")
for sentence in sentences:
    words = get_matches_for_pattern(                       3
        word_pattern,
        sentence
    )
    print(words)

  • 1 A new function to do the pattern-matching
  • 2 Now you can ask the function to do the hard work.
  • 3 You can reuse the function whenever you need to.

In the updated parsing code, it’s more clear that the review is being broken into pieces. With well-named variables and a clear, short for loop, the two-stage structure of the process is also clear. Someone looking at this code later will be able to read the main code, only digging into how get_matches_for_pattern works if they’re curious or want to change it. Abstraction has introduced clarity and code reuse into this program.

3.1.3. Abstraction is a simplifier

I want to emphasize that abstraction is useful for making code easier to understand; it achieves this by keeping the intricacies of some functionality hidden away until you want to know more. This is a technique used in writing technical documentation as well as designing the interfaces used to interact with code libraries.

Understanding code is much like understanding a passage from a book. A passage has many sentences, which are like the lines of code. In any given sentence, you may find a word with which you’re unfamiliar. In software, this might be a line of code that does something new or different than you’re used to. When you find such words in books, you might look them up in the dictionary. The only equivalent when dealing with lengthy procedures is diligent code commenting.

One way you can tackle this is by abstracting related bits of your code into functions that clearly state what they do. You saw this in listings 3.1 and 3.2. The function get_matches_for_pattern gets the matches for a given pattern from a string. Before it was updated, though, the intent of that code was not so clear.

Tip

In Python, you can add additional context to a module, class, method, or function using docstrings. Docstrings are special lines near the beginning of these constructs that can tell the reader (as well as some automated software) how the code behaves. You can read more about docstrings on Wikipedia (https://en.wikipedia.org/wiki/Docstring).

Abstraction reduces cognitive load, the amount of effort required by your brain to think about or remember something, so that you can spend your time making sure your software does what it needs to do!

3.1.4. Decomposition enables abstraction

As I mentioned in chapter 2, decomposition is the separation of something into its constituent components. In software, that means doing the kinds of things you saw earlier: separating sections of code that do a single thing into functions. In fact, it also relates to the discussion on design and workflow from chapter 1. The common theme here is that software written in small parts that work in tandem often leads to more maintainable code than software written in one large blob. You’ve seen that this can help reduce cognitive load and make code easier to understand. Figure 3.4 shows how a huge system can be decomposed all the way down to achievable tasks.

Figure 3.4. Decomposition into granular components eases understanding.

See how the pieces get smaller from left to right? Trying to build something big in one piece like the left side is like packing your whole house in a shipping container. Building things like the right side is like organizing each room of your house into small boxes you can carry. Decomposition helps you handle big ideas in small increments.

3.2. Encapsulation

Encapsulation is the basis for object-oriented programming. It takes decomposition one step further: whereas decomposition groups related code into functions, encapsulation groups related functions and data into a larger construct. This construct acts as a barrier (or capsule) to the outside world. What constructs are available in Python?

3.2.1. Encapsulation constructs in Python

Most often, encapsulation in Python is done with a class. In classes, functions become methods; methods are similar to functions, but they are contained in a class and often receive an input that is either an instance of the class or the class itself.

In Python, modules are also a form of encapsulation. Modules are even higher-level than classes; they group multiple related classes and functions together. For example, a module dealing with HTTP interactions could contain classes for requests and responses, as well as utility functions for parsing URLs. Most *.py files you encounter would be considered modules.

The largest encapsulation available in Python is a package. Packages encapsulate related modules into a directory structure. Packages are often distributed on the Python Package Index (PyPI) for others to install and reuse.

Take a look at figure 3.5 and notice that the pieces of the shopping cart are decomposed into distinct activities. They’re also isolated; they don’t depend on each other to perform a task. Any cooperation between activities is coordinated at the higher shopping-cart level. The shopping cart itself is isolated inside the e-commerce application; any information it needs will be passed into it. You can think of encapsulated code as having a castle wall around it, where the functions and methods are the drawbridge for getting in or out.

Figure 3.5. By decomposing a system into small parts, you can encapsulate behaviors and data into isolated pieces. Encapsulation encourages you to reduce the responsibilities of any given portion of code, helping you avoid complicated dependencies.

Which of these pieces do you think would be a

  • Method?
  • Class?
  • Module?
  • Package?

The three smallest pieces—calculating tax, calculating shipping, and subtracting a discount—would likely be methods inside a class that represents the shopping cart. The e-commerce system seems like it could have enough functionality to be a package because the shopping cart is just one part of that system. Different modules within the package could arise depending on how closely related they are to each other. But how do they work together if they’re each surrounded by a castle wall?

3.2.2. Expectations of privacy in Python

Many languages formalize the “castle wall” aspect of encapsulation by introducing the concept of privacy. Classes can have private methods and data that can’t be accessed by anyone but instances of the class. This is in contrast to public methods and data, which are often referred to as the interface of the class because this is how other classes interface with it.

Python has no true support for private methods or data. Instead, it follows a philosophy of trusting developers to do the right thing. A common convention does help in this arena, though. Methods and variables intended for use within a class but not from outside the class are often prefixed with an underscore. This provides a hint to future developers that a particular method or variable isn’t intended as part of the public interface of the class. Third-party packages often state loudly in their documentation that such methods are likely to change from version to version and should not be explicitly relied on.

In chapter 2, you learned about coupling between classes, and that loose coupling is the desired state. The more methods and data a particular class depends on from another class, the more coupled they become. This is magnified when a class depends on the internals of another class because that means most of the class can’t be improved in isolation without the risk of breaking other code.

Abstraction and encapsulation work together by grouping related functionality together and hiding the parts of it that don’t matter to anyone else. This is sometimes called “information hiding,” and it allows the internals of a class (or system in general) to change rapidly without other code having to change at the same rate.

3.3. Try it out

I’d like you to get some practice with encapsulation now. Suppose you’re writing code to greet new customers to an online store. The greeting makes customers feel welcome and offers them an incentive to stick around. Write a greeter module that contains a single class, Greeter, that has three methods:

  1. _day(self)—Returns the current day (Sunday, for example)
  2. _part_of_day(self)—Returns “morning” if the current hour is before 12 P.M., “afternoon” if the current hour is 12 P.M. or later but before 5 P.M., and “evening” from 5 P.M. onward
  3. greet(self, store)—Given the name of a store, store, and the output of the previous two methods, prints a message of the form
     Hi, welcome to <store>!
     How’s your <day> <part of day> going?
     Here’s a coupon for 20% off!

The _day and _part_of_day methods can be signified as private (named with a leading underscore) because the only functionality the Greeter class needs to expose is greet. This helps encapsulate the internals of the Greeter class so that its only public concern is performing the greeting itself.

Tip

You can use datetime.datetime.now() to get the current datetime object, using the .hour attribute for the time of day and .strftime('%A') to get the current day of the week.

How did it go? Your solution should look something like the following example.

Listing 3.3. A module to generate greetings for an online store
from datetime import datetime

class Greeter:
    def __init__(self, name):
        self.name = name

    def _day(self):                            1
        return datetime.now().strftime('%A')

    def _part_of_day(self):                    2
        current_hour = datetime.now().hour

        if current_hour < 12:
            part_of_day = 'morning'
        elif 12 <= current_hour < 17:
            part_of_day = 'afternoon'
        else:
            part_of_day = 'evening'

        return part_of_day

    def greet(self, store):                    3
        print(f'Hi, my name is {self.name}, and welcome to {store}!')
        print(f'How's your {self._day()} {self._part_of_day()} going?')
        print('Here's a coupon for 20% off!')

    ...

  • 1 Formats the datetime to get the current day name
  • 2 Determines the part of day based on the current hour
  • 3 Prints the greeting using all the calculated bits

The Greeter prints the desired message, so everything’s great, right? If you look carefully, though, the Greeter knows how to do too much. The Greeter should only greet people; it shouldn’t be responsible for determining the day of the week and what part of the day it is! The encapsulation isn’t ideal. What are you to do?

3.3.1. Refactoring

Encapsulation and abstraction are often iterative processes. As you write more code, constructs that made sense before may seem awkward or forced. I assure you that this is totally natural. The feeling that your code is working against you might mean it’s time to refactor. Refactoring code means updating how it’s structured to serve your needs more effectively. When you refactor, you will often need to change the ways you represent behaviors and concepts. Moving data and implementations around is a necessary part of improving the code. It’s kind of like rearranging the living room every few years to fit your current mood.

Refactor your Greeter code now by moving the methods for getting information about the day and time out of the Greeter class and making them standalone functions within the module.

The functions never used the self argument when they were methods, so they’ll look pretty much the same but without that argument:

def day():
    return datetime.now().strftime('%A')

def part_of_day():
    current_hour = datetime.now().hour

    if current_hour < 12:
        part_of_day = 'morning'
    elif 12 <= current_hour < 17:
        part_of_day = 'afternoon'
    else:
        part_of_day = 'evening'

    return part_of_day

The Greeter class can then call these functions by referencing them directly instead of with the self. prefix:

class Greeter:
    ...

    def greet(self, store):

        print(f'Hi, my name is {self.name}, and welcome to {store}!')
        print(f'How's your {day()} {part_of_day()} going?')
        print('Here's a coupon for 20% off!')

Now the Greeter only knows the information it needs to make a greeting, without worrying about the details of how to get that information. What’s also nice is that the day and part_of_day functions can be used elsewhere if needed, without having to reference the Greeter class. That’s two benefits in one!

Eventually, you might develop more datetime-related features, at which point it could make sense to refactor all those features into their own module or class. I often wait to do this until several functions or classes present a clear relationship, but some developers like to do this from the start to be strict about keeping things separate.

3.4. Programming styles are an abstraction too

A number of programming styles (or paradigms) have become popular over the years, often sprouting out of a particular business domain or user base. Python supports several styles, and they are abstractions in their own ways. Remember that abstraction is the act of storing concepts away so they can be digested easily. Each programming style stores information and behavior a bit differently. No one style is “right,” but some are better than others at tackling specific problems.

3.4.1. Procedural programming

I’ve discussed and shown some examples of procedural programming in this and previous chapters. Procedural software prefers to operate using procedure calls, which we tend to call “functions.” These functions aren’t encapsulated into classes, so they often rely only on their inputs and occasionally on some global state.

NAMES = ['Abby', 'Dave', 'Keira']

def print_greetings():                       1
    greeting_pattern = 'Say hi to {name}!'
    nice_person_pattern = '{name} is a nice person!'
    for name in NAMES:
        print(greeting_pattern.format(name=name))
        print(nice_person_pattern.format(name=name))

  • 1 A standalone function that relies only on NAMES

If you’re fairly new to programming, this style will likely feel familiar because it’s a common jumping-off place. Going from one long procedure to a procedure that calls a few functions tends to feel natural, so it’s a good approach to teach first. The benefits of procedural programming strongly overlap with those discussed in section 3.1.4 because procedural programming focuses heavily on functions.

3.4.2. Functional programming

Functional programming sounds like it would be the same as procedural programming—function is right there in the name! But although it’s true that functional programming relies heavily on functions as the form of abstraction, the mental model is quite different.

Functional languages require you to think about programs as compositions of functions. for loops are replaced by functions that operate on lists, for example. In Python, you might write the following:

numbers = [1, 2, 3, 4, 5]
for i in numbers:
    print(i * i)

In a functional language, you might write it like this:

print(map((i) => i * i, [1, 2, 3, 4, 5]))

In functional programming, functions sometimes accept other functions as arguments or return them as results. This is seen in the previous snippet; map accepts an anonymous function that takes one argument and multiplies it by itself.

Python has a number of functional programming tools; many of these are available using built-in keywords, and others are imported from built-in modules like functools and itertools. Though Python supports functional programming, it isn’t often a preferred approach. Some common features of functional languages, like the reduce function, have been moved to functools.

Many feel that the imperative Python way of performing some of these operations is more clear. Using functional Python features would look like this:

from functools import reduce

squares = map(lambda x: x * x, [1, 2, 3, 4, 5])
should = reduce(lambda x, y: x and y, [True, True, False])
evens = filter(lambda x: x % 2 == 0, [1, 2, 3, 4, 5])

The preference in Python would be the following:

squares = [x * x for x in [1, 2, 3, 4, 5]]
should = all([True, True, False])
evens = [x for x in [1, 2, 3, 4, 5] if x % 2 == 0]

Try each approach and print the variables afterward. You’ll see that they produce identical results; it’s up to you to use the style you find most understandable.

One functional feature of Python I enjoy is functools.partial. This function allows you to create a new function from an existing function with some of the original function’s arguments set. This is sometimes clearer than writing a new function that calls the original function, especially in cases where a general-use function behaves like a more specifically named function. In the case of raising numbers to a power, x to the power of 2 is commonly called the square of x, and x to the power of 3 is commonly called the cube of x. You can see how this works in Python with the partial helper:

from functools import partial

def pow(x, power=1):
    return x ** power

square = partial(pow, power=2)  1
cube = partial(pow, power=3)    2

  • 1 A new function, square, that acts like pow(x, power=2)
  • 2 A new function, cube, that acts like pow(x, power=3)

Using familiar names for behaviors can help a great deal for those reading your code later down the line.

Functional programming used carefully can offer a number of performance benefits compared to procedural programming, making it useful in computationally expensive areas like mathematics and data simulation.

3.4.3. Declarative programming

Declarative programming focuses on declaring the parameters of a task without specifying how to accomplish it. The details of accomplishing the task are mostly or fully abstracted from the developer. This is useful when you need to repeat a highly parametric task with only slight variations to the parameters. Often this style of programming is realized via domain-specific languages (DSLs). DSLs are languages (or language-like markup) that are highly specialized for a specific set of tasks. HTML is one such example; developers can describe the structure of the page they want to create without saying anything about how a browser should convert a <table> to lines and characters on a screen. Python, on the other hand, is a general-purpose language that can be used for many purposes and requires direction from a developer.

Consider exploring declarative programming when your software lets users do something highly repetitive, like translating code to another system (SQL, HTML, and so on) or creating multiple similar objects for repeated use.

A widely used example of declarative programming in Python is the plotly package. Plotly lets you create graphs from data by describing the type of graph you’d like. An example from the plotly documentation (https://plot.ly/python/) looks like this:

import plotly.graph_objects as go

trace1 = go.Scatter(                            1
    x=[1, 2, 3],                                2
    y=[4, 5, 6],                                3
    marker={'color': 'red', 'symbol': 104},     4
    mode='markers+lines',                       5
    text=['one', 'two', 'three'],               6
    name='1st Trace',
)

  • 1 Declares the intent to build a scatter plot
  • 2 Declares the shape of the x-axis data
  • 3 Declares the shape of the y-axis data; easy to compare to x
  • 4 Declares the line marker appearance
  • 5 Declares that markers and lines will be used in the plot
  • 6 Declares the tooltip text for each marker

This sets the data for the plot, as well as the visual characteristics. Each desired outcome is declared instead of being added procedurally.

For comparison, imagine a procedural approach. Instead of supplying several pieces of configuration data to a single function or class, you would instead perform each configuration step as an independent line of a longer procedure:

trace1 = go.Scatter()
trace1.set_x_data([1, 2, 3])         1
trace1.set_y_data([4, 5, 6])
trace1.set_marker_config({'color': 'red', 'symbol': 104, 'size': '10'})
trace1.set_mode('markers+lines')
...

  • 1 Each piece of information is set explicitly with methods.

Declarative style can provide a more succinct interface when a lot of configuration is to be done by the user.

3.5. Typing, inheritance, and polymorphism

When I talk about typing here, I don’t mean typing on a keyboard. A language’s typing, or type system, is how it chooses to manage data types of variables. Some languages are compiled and check data types at compilation time. Some check types at runtime. Some languages infer the data type of x = 3 to be an integer, whereas others require int x = 3 explicitly.

Python is a dynamically typed language, meaning that it determines its data types at runtime. It also uses a system called duck typing, whose name comes from the idiom, “If it walks like a duck and it quacks like a duck, then it must be a duck.” Whereas many languages will fail to compile your program if it references an unknown method on a class instance, Python will always attempt to make the method call during execution, raising an AttributeError if the method doesn’t exist on the instance’s class. Through this mechanism, Python can achieve a degree of polymorphism, which is a programming language feature where objects of different types provide specialized behavior via a consistent method name.

At the advent of object-oriented programming, there was a race to model full systems as cascades of inherited classes. ConsolePrinter inherited from Printer, which inherited from Buffer, which inherited from BytesHandler, and so on. Some of these hierarchies made sense, but many resulted in rigid code that was difficult to update. Trying to make one change could lead to a massive ripple of changes all the way up or down the tree.

Today, the preference has shifted to composing behaviors into an object. Composition is the converse to decomposition; pieces of functionality are brought together to realize a complete concept. Figure 3.6 contrasts a more rigid inheritance structure with one where objects are composed of many traits. A dog is a quadruped, a mammal, and a canine. With inheritance, you would be forced to create a hierarchy from these. All canines are mammals, so that seems fine, but not all mammals are quadrupeds. Composition frees you from the limitations of a hierarchy while still providing the concept of relatedness between two things.

Figure 3.6. Inheritance versus composition

Composition is often done through a language feature called an interface. Interfaces are formal definitions of methods and data that a particular class must implement. A class can implement multiple interfaces to broadcast that it has the union of those interfaces’ behaviors.

Python lacks interfaces. Oh no! How can you avoid a deep inheritance hierarchy? Fortunately, Python makes this possible through the duck typing system as well as multiple inheritance. Whereas many statically typed languages allow a class to inherit from only one other class, Python can support inheritance from an arbitrary number of classes. Something like an interface can be built using this mechanism, and in Python it’s often referred to as a mixin.

Suppose you want to create a model for a dog that can speak and roll over. You know you’ll eventually want to model other animals that can also do tricks, so to make these behaviors into something like an interface, you can name them with a Mixin suffix to be clear about your intent. With those behavior mixins in place, you’ll be able to make a Dog class that can speak and roll_over, as shown in the following listing, with the freedom to let your future animals speak or roll_over using the same approach.

Listing 3.4. Multiple inheritance providing interface-like behavior
class SpeakMixin:                            1
    def speak(self):
        name = self.__class__.__name__.lower()
        print(f'The {name} says, "Hello!"')

class RollOverMixin:                         2
    def roll_over(self):
        print('Did a barrel roll!')

class Dog(SpeakMixin, RollOverMixin):        3
    pass

  • 1 Speaking behavior is encapsulated in SpeakMixin to show it’s composable.
  • 2 The roll-over behavior in RollOverMixin is composable too.
  • 3 Your Dog can speak, roll_over, and whatever else you teach it.

Now that Dog has inherited from some mixins, you can check that your dog knows a couple of tricks:

dog = Dog()
dog.speak()
dog.roll_over()

You should see this output:

The dog says, "Hello!"
Did a barrel roll!

The fact that the dog knows English is suspect, but otherwise this checks out. We’ll take a deeper dive into inheritance and some other related concepts in chapters 7 and 8, so sit tight!

3.6. Recognizing the wrong abstraction

Almost as useful as applying abstraction to new code is recognizing when abstractions in existing code aren’t working. This could be because new code has proven that the abstraction doesn’t fit all use cases, or it could be that you see a way to make the code clearer with a different paradigm. Whatever the case, taking the time to care for the code is a task others will appreciate, even if they don’t realize it explicitly.

3.6.1. Square pegs in round holes

As I’ve said, abstraction should be leveraged to make sure things are clearer and easier. If an abstraction causes you to bend over backward just to make something work, consider updating it to remove the friction or replace it with a new approach altogether. I’ve gotten pretty far into new code trying to make it work with what was in place, only to realize it would be easier to change the environment than adapt to it. The trade-offs here are time and effort, both in rewriting the code and making sure it still works. That up-front time you spend might save everyone time in the long run, though.

If the interface to a third-party package causes friction, and you’re not in a position to spend time or effort updating their code, you can always consider creating an abstraction around that interface for your own code to use. This is often called an adapter in software, and I liken it to using one of those airport travel plugs in another country. You certainly can’t change the electrical plugs in France (without someone getting angry, anyway) and you don’t have a French plug for your devices on-hand. So even though the travel plug costs €48 and your first-born, it’s less expensive than finding and buying French power supplies for three or four different devices. In software, you can create your own adapter class that has the interface your program expects, with code in each of its methods that makes calls to the incompatible third-party object behind the scenes.

3.6.2. Clever gets the cleaver

I’ve gone on about writing code that’s slick, but overly clever solutions can be painful too. If such solutions provide too much magic and not enough granularity, you might find that other developers create their own solutions to get their jobs done, defeating your effort to provide a single working implementation. Robust software must weigh the frequency and impact of use cases to determine which to accommodate; common use cases should be as smooth as possible, whereas rare use cases can be clunky or explicitly unsupported if needed. Your solution should be just clever enough, which is an admittedly hard target to hit.

That being said, if something feels awkward or cumbersome, give it some time. If it still feels awkward or cumbersome after a while, ask others if they agree. If they say no but it still feels awkward or cumbersome, it’s probably awkward or cumbersome. Go forth and make the world a little better with abstraction!

Summary

  • Abstraction is a tool for deferring obligatory comprehension of code.
  • Abstraction takes many forms: decomposition, encapsulation, programming style, and inheritance versus composition.
  • Each approach to abstraction is useful, but context and extent of use are important considerations.
  • Refactoring is an iterative process; abstraction that once worked may need to be revisited later.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.142.85