National Academies Press: OpenBook

Guidance for Development and Management of Sustainable Enterprise Information Portals (2018)

Chapter: Section 4 - Next Generation EIP: Microservices Architecture

« Previous: Section 3 - Industry Practices for Sustainable EIPs
Page 38
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 38
Page 39
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 39
Page 40
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 40
Page 41
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 41
Page 42
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 42
Page 43
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 43
Page 44
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 44
Page 45
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 45
Page 46
Suggested Citation:"Section 4 - Next Generation EIP: Microservices Architecture." National Academies of Sciences, Engineering, and Medicine. 2018. Guidance for Development and Management of Sustainable Enterprise Information Portals. Washington, DC: The National Academies Press. doi: 10.17226/24999.
×
Page 46

Below is the uncorrected machine-read text of this chapter, intended to provide our own search engines and external engines with highly rich, chapter-representative searchable text of each book. Because it is UNCORRECTED material, please consider the following text as a useful but insufficient proxy for the authoritative book pages.

38 Next Generation EIP: Microservices Architecture Based on industry practice recommendations, sustainable DOT EIPs will need to move away from monolithic and heavily integrated portal applications to more distributed hardware and software portal applications. To remain sustainable, agencies will need to adopt distributed, autonomous processes and governance to manage each portal application. To support these recommendations, architectures based on the SOA model are typically used. One variation of the SOA architecture, called the microservices architecture pattern, has become predominant in the last few years and is being rapidly adopted to design large, scalable, portal applications containing multiple independent applications. The microservices architec- ture pattern, often referred to as microservices, is a modern interpretation of SOA and a novel approach to application development focusing on making a system replaceable rather than maintainable. The microservices architecture pattern recommends that large applications be built as a suite of small and modular services, with each module supporting a specific business goal using a simple, well-defined interface to communicate with other modules. Modular Space System Analogy to Microservices Architecture Microservices architecture follows a system engineering concept called System of Systems. The Modular Space System by the Defense Advanced Research Projects Agency (DARPA) applies this concept by migrating to interconnected, autonomous mini satellites (CubeSats) rather than the traditional development of a single large, complex, tightly integrated, and redundant satellite. Each mini satellite is independent, low cost, and has a specific functionality (e.g., power, communication, and sensor). The mini satellites, as a collective, perform func­ tions comparable to a traditional satellite, while each one is disposable and easily upgraded with newer modules. This migration offers greater resiliency, maintainability, and cost efficiencies. Microservices architecture is an approach to developing a single application as a suite of small services, each running its own process and communicating with lightweight mechanisms. Each service is independent, and therefore services can be written in different programming languages and can use different data storage technologies. Services are built around business capabilities, are independently deployable using fully automated deployment, and require a bare minimum of centralized management. Section 4.1 highlights the characteristics of microservices architecture. Section 4.2 presents eight key points to consider when adopting a microservices architecture. Section 4.3 concludes S E C T I O N 4

Next Generation EIP: Microservices Architecture 39 by highlighting the evolutionary nature of development in the microservices framework, focusing on automating fast, frequent, and well-controlled software changes.7 4.1 Characteristics of Microservices Architecture Microservices architecture is a more granular evolution of SOA. Its goal is to decompose a soft- ware application into elementary software components to facilitate agile development and deploy- ment. Large companies, such as Amazon, eBay, and Netflix, have already adopted microservices architecture as their main architecture. Microservices architecture came about as a solution to the challenges developers faced when upgrading large integrated applications such as EIPs. In a monolithic architecture, the entire architecture had to be rebuilt with each small change, and this meant that rebuilds weren’t happening rapidly. In a microservices architecture, each microservice runs a unique process and usually manages its own database. This model not only provides development teams with a more decentralized approach to building software, it also allows each service to be deployed, rebuilt, redeployed, and managed independently from the rest of the enterprise software. Figure 4-1 shows a graphical comparison of monolithic and microservices architectures. In a monolithic architecture, software components are tightly integrated as part of an application that is replicated across a server cluster to allow application scalability. In a microservices architecture, each software component is standalone and is loosely integrated with the other components. Each component is deployed independently across a server cluster, and each component can be scaled independently across the cluster. This allows applications based on a microservices architecture to be reactive and adaptive to the user demand changes rapidly and without redesign. 7This section is based on “Microservices: A Definition of this New Architectural Term” (Fowler and Lewis, 25 March 2014). https://martinfowler.com/articles/microservices.html Figure 4-1. Monolithic versus microservices architecture.

40 Guidance for Development and Management of Sustainable Enterprise Information Portals Figure 4-2. Example of a microservices implementation of a DOT EIP on cloud infrastructure. In a typical microservices architecture implementation, microservices are paired with auto- scalers and load balancers. Microservices can then be cloned or decommissioned rapidly to respond to fluctuating user requests in real time. Microservices architecture patterns are intended to be scalable in three ways: • Scale by decomposition into simple functions (microservices) • Scale by cloning microservices on demand • Scale by partitioning data across a cluster Figure 4-2 shows a possible implementation of a DOT EIP using microservices architecture patterns on a cloud infrastructure. A microservices architecture pattern is also a more platform-agnostic approach to applica- tion development. Microservices architecture relies heavily on containerization, which allows microservices to be developed completely independently from each other using different lan- guages and libraries. Only the external interfaces need to be standardized across all microservices. Shipping Containers Analogy to Application Containers Malcom McLean revolutionized the transportation system by creating the inter­ modal shipping container to standardize the movement of goods among ships, trains, and trucks. This innovation increased both the speed and security of trans­ portation. The same container can be used end­to­end, eliminating the need to unload and reload content at intermodal transfers. Moreover, the container can be locked during its journey, reducing frequency for content verification. Similarly, software containers provide an abstraction by which applications can more quickly and securely move from the developer’s laptop to a staging/testing environment and finally on to production with minimal interaction and tinkering along the way.

Next Generation EIP: Microservices Architecture 41 4.2 Considerations When Adopting a Microservices Architecture While the microservices architecture pattern brings many advantages, it also greatly increases application complexity. Microservices architecture essentially trades code complexity for opera- tional complexity. A major drawback of a microservices architecture is the increased operational complexity generated by the addition of interprocess communication and the occurrence of par- tial failure across the distributed system. Another microservices architecture pattern drawback is data partitioning. The distribution of data across multiple processes makes data consistency much more challenging to achieve. Also, some data operations that are trivial on monolithic systems are not at all supported on a system with distributed data. Major upgrades, involving more than one microservice, can also be challenging because the interactions between each of the microservices can easily bring about unexpected application behaviors. Consequently, services interactions need to be carefully considered during testing, and deployment needs to be very carefully coordinated. To alleviate this increased operational complexity, most microservices architecture pattern implementations rely on automated services to monitor, deploy, and manage their clusters. Scripting approaches have also been developed to programmatically control the configuration and behavior of microservices clusters and allow them to be rapidly modified or corrected. 4.2.1 Design for Failure Microservices architecture uses multiple services as software components, and, consequently, microservices-based applications need to be designed so that they can tolerate the failure of ser- vices. In a commodity environment where a large amount of inexpensive hardware is running, connecting all application services, any service call could fail due to the unavailability of a net- work or the failure of a server component. Therefore, the application clients need to be designed to respond to the lack of response as gracefully as possible. This need introduces additional complexity to handling failure as compared to the way traditional monolithic architectures are designed to handle it. With microservices architecture, software development teams will need to constantly reflect on how service failures affect the user experience and anticipate these failures. It is common practice in companies currently using microservices architecture to purposely induce services or even data center failures on production systems during the working day to test both applica- tions’ resilience and monitoring. 4.2.2 Organized Around Business Capabilities Rather than Technology Traditionally, large EIPs are developed by different teams, each focusing on an EIP technology layer. Teams involved may include a user interface application team, a business logic application team, and a database application team. Each team handles and manages the needs of each busi- ness unit within the team’s technology layer. With this kind of organizational structure, simple changes to an EIP application can lead to extensive cross-team collaboration and can sometimes lead to time-consuming cross-team communication and expensive delays. To avoid these delays and be more reactive, microservices architecture approaches team and application (services) divi- sion in a different way. Rather than organizing applications (services) and teams around tech- nology layers and team member skills, applications (services) and teams are organized around business capabilities. Cross-functional teams (including the project manager, user-experience experts, and business logic and database specialists) are created to support each business area EIP application. Each cross-functional team becomes the owner of the EIP application from the earliest stages of development and testing to its deployment in production and maintenance.

42 Guidance for Development and Management of Sustainable Enterprise Information Portals Like most enterprises or agencies, DOTs do not typically use cross-functional teams for the development of their EIP application, and they clearly separate (1) development and (2) operations and maintenance into two distinct groups. Often contractors are used for development, and an internal IT group is used for maintenance and operations. There may be significant challenges for the current DOT IT workforce in reshaping its teams to be more cross functional. 4.2.3 Products Rather than Projects Enterprise software acquisition often follows the following established process: • An IT project team is contracted or tasked to develop software based on a set of pre-established requirements. • Once the software is completed and approved, the team hands it over to an IT operations and maintenance team within the organization. • The project team is then disbanded, and members of the team join other teams to develop new software. Microservices architecture relies heavily on cloud infrastructure and its automation of software deployment and operations and therefore reduces considerably the work needed to be performed by IT maintenance and operation teams within an organization. This reduction in in-house IT management has led proponents of microservices architecture to modify the traditional IT project development process to establish the software development team as the sole owner of the soft- ware during its entire lifecycle, incorporating not only software development but also deployment, operation, and maintenance as part of team responsibilities. This approach creates the notion that a development team should own a product over its lifetime. Amazon.com and Netflix are examples of companies that have already implemented this approach across their development teams. This approach is often referred to as “you build it, you run it” to emphasize the fact that a development team needs to take full responsibility for the software all the way to production. This approach has the advantage of bringing developers into day-to-day contact with how their software behaves in production and how its users behave, rather than getting delayed second-hand information. It also forces developers to look at the software as a tool to assist users with enhancing business capability, rather than as a set of functionalities to be completed. 4.2.4 Smart Endpoints and Dumb Pipes Traditional communication in EIPs is implemented through a smart and complex messaging process handling, decomposing, and rerouting requests and responses to the various EIP com- ponents. An enterprise service bus (ESB) is an example of such a centralized message handler. It often includes sophisticated facilities for message routing, choreography, transformation, and the application of business rules. In microservices architecture, decentralization prevails and business logic is no exception. Applications built following a microservices architecture aspire to be as decoupled and as cohesive as possible and, as such, own their own domain logic and implement their own busi- ness rules internally, eliminating the need for a centralized business logic and messaging process. Therefore, messaging services within a microservices architecture use simple communication protocols such as representational state transfer (REST) rather than relying on complex proto- cols (such as business process execution language [BPEL]) and centralized tools to orchestrate all business rules. This alternative approach is often referred to as smart endpoints (microservices) and dumb pipes (dumb as in acts as a message router only). This decentralized approach may prove to be challenging for agencies and enterprises that include business and messaging rules as part of their centralized governance.

Next Generation EIP: Microservices Architecture 43 4.2.5 Many Languages, Many Options Traditional EIPs, with a monolithic architecture, usually use a single language and tend to have a limited number of technologies implemented across their application to ease integration and testing. Decentralization in microservices architecture allows for application software compo- nents (microservices) to run independently from other application components and interact only through a network protocol. This means that, internally, application components (microservices) can use any language or libraries without affecting other components of the system. The component (microservices) development team is then able to pick the language and technologies (libraries) that are the most adapted to the microservices functionalities (for exam- ple, JavaScript for user interface [UI] microservices, Python for data processing microservices) and that the team is the most familiar with. This allows for the development of components (microservices) that optimal languages and technologies to fulfill their function and are better developed and maintained by the development team. This decentralization of software devel- opment technology may prove to be challenging for agencies and enterprises. It could lead to a human resources problem if knowledge of the technologies used to develop a microservice is scarce across the organization. Furthermore, the decentralization of software development technology could conflict with already established and tightly controlled centralized governance specifying limited programming languages and technologies. 4.2.6 Decentralized Governance The decentralization characteristic of microservices architecture, as discussed previously, con- flicts with the tendency to standardize on single-technology platforms followed by most centralized governance. The traditional approach followed by centralized governance is often to enact tech- nologies and standards requirements to guide the development of a system within an organization. Experience shows that this approach is often constricting, as it is very difficult to find technologies and standards that fulfill the needs of each one of the applications running in an organization. Rather than following a set of defined standards and technologies established by a high-level commission and specified in technical reference model (TRM) or Standards Information Base (SIB) documents, microservices architecture practitioners have adopted a different approach. They prefer the idea of producing useful tools that other developers can use to solve similar problems. These tools are usually harvested from implementations and shared with a wider group, sometimes, but not exclusively, using an internal (in-house) open source model. Sharing useful and tested code, such as libraries, encourages developers to solve similar prob- lems in similar ways, yet leaves the door open to picking a different approach if required. Shared libraries tend to be focused on common problems of data storage, interprocess communication, and infrastructure automation. Microservices architecture practitioners also tend to favor a more flexible and less strict way to design EIPs and often would rather implement simple patterns that can be easily modified over imposing standardized tools. Patterns like Tolerant Reader and Consumer-Driven Contracts are often applied to microservices. These assist service contracts with evolving independently. Tolerant Reader is an integration pattern that helps in creating robust communication systems. The idea is to be as tolerant as possible when reading data from another service. This way, when the communication schema changes, the readers will not break. Consumer-driven contracts are a pattern for evolving services. In consumer-driven contracts, each consumer captures their expectations of the provider in a separate contract. All of these contracts are shared with the provider so that the provider gains insight into the obligations it must fulfill for each individual client. The provider can create a test suite to validate these obligations. This allows the provider

44 Guidance for Development and Management of Sustainable Enterprise Information Portals to stay agile and make changes that do not affect any consumer and locate consumers that will be affected by a required change for deeper planning and discussion. These patterns allow software development teams to define the contract for a service. These contracts become part of the automated build before code for the new service is even written. The service is then built out only to the point where it satisfies the contract. These techniques and the tooling growing up around them provide a way to keep applications simple and limit the need for central contract management by decreasing the temporal coupling between services— the start of work on one service will not have to wait for the completion of another service. 4.2.7 Decentralized Data Management Microservices architecture also raises the issue of decentralizing data storage. Traditional EIPs typically prefer to store persistent data in a single database across a range of applications. Often the decision to do this is driven by vendors’ commercial models around licensing. With microservices architecture the preference is to let each microservice manage its own database, either different instances of the same database technology or entirely different database systems. This approach is sometimes called polyglot persistence. This approach allows each service to use the database solution that is the most appropriate for its functionalities. For example, web UI microservices can use in-memory databases using REST API while a GIS analysis microservice can use a geospatial relational database using standard Structured Query Language (SQL). Figure 4-3 shows a diagram representing the different database approaches used by monolithic and microservices architectures. In a monolithic architecture, a single database hosts the data used by all the components of the application. In a microservices architecture, each micro service uses its own database and shares it with similar microservices. Decentralizing responsibility for data across microservices has implications for managing updates. The common approach to dealing with updates has been to use transactions to guarantee consistency when updating multiple resources. Using transactions helps with consistency, but imposes significant temporal coupling, which is problematic across multiple services. Distributed transactions are notoriously difficult to Figure 4-3. Monolithic database versus microservices databases.

Next Generation EIP: Microservices Architecture 45 implement; therefore, microservices architectures emphasize transactionless coordination between services, with the explicit recognition that consistency may only be eventual consistency and that problems will be dealt with by compensating operations. Managing data inconsisten- cies in this way is a new challenge for many development teams, but it is one that often matches business practices. Often businesses accept a degree of database inconsistency in order to respond quickly to demand, while having some kind of reversal process to deal with mistakes. This trade- off approach offers value as long as the cost of fixing mistakes is less than the cost of lost business or service. 4.2.8 Infrastructure Automation and Continuous Integration Infrastructure automation techniques have evolved considerably over the last few years. This evolution has greatly reduced the operational complexity of building, deploying, and operating software. With traditional monolithic EIPs, a method called continuous integration is often used to manage deployment and ensure application readiness. In this method, the various changes made by development team members on each component of their application are integrated as often as possible. This method leads to a significant reduction in integration problems and allows teams to develop cohesive software more rapidly. Continuous integration is a software development practice whereby members of a team inte- grate their work frequently (usually each person integrates at least daily), leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Microservices architecture practitioners are less constrained in their design and development by tight integration requirements outside of a single microservice. While still applying continu- ous integration methods within microservices, microservices architecture practitioners have developed a new method to take the deployment process a step further. This concept is called continuous delivery. Continuous delivery is a software development discipline in which the development team builds software so that it can be released to production at any time and by anyone on the team. Continuous delivery is based on the following principles: • Software is deployable throughout its lifecycle. • Priority is given to keeping the software deployable over working on new features. • Anybody can get fast, automated feedback on the production readiness of their systems any time somebody makes a change to them. • There are push-button deployments of any version of the software to any environment on demand. To implement this method, microservices development teams make extensive use of infra- structure automation techniques to gain as much confidence as possible that their software is working by running many automated tests and automating the promotion and deployment of software through the various stages of their build pipeline. Figure 4-4 shows an example of an automated build pipeline where software is being pushed automatically through various stages Deploy on Build Machine Deploy to Integration Test Env. Deploy to UAT Deploy to Production Deploy to Performance Test Env. Compile, Run, and Functional Tests Figure 4-4. Example of a basic automated build pipeline.

46 Guidance for Development and Management of Sustainable Enterprise Information Portals of the pipeline. At each stage, automated testing is performed before allowing the software to be promoted to the next stage. This kind of automated testing in production is a radical change from the traditional develop- ment pipeline, acceptance, and promotion methods. This level of automation is likely to make many in an IT operations group very uncomfortable. But in the case of microservices applica- tion, services are designed to handle failure occurring in the system and restore service automati- cally (by automatically rolling back to the last known working service, for example). This agility greatly reduces the risks and consequences of hastily rolling out erroneous code into production. In a microservices architecture preference is given to deploying first and correcting later rather than delaying deployment until completely ready for production. One of the main reasons for this approach is that microservices by nature are difficult to test outside of production, as their behavior depends greatly on the application user demands at scale. Nonetheless, the correction of failing services needs to be accomplished as rapidly as possible after deployment. To catch failure as early as possible, microservices development teams need real-time monitoring capabilities for application processes and their associated business metrics. For this reason, it is important that microservices-based applications implement sophisticated real-time monitoring and early warning systems so that development teams can promptly follow up on and investigate things that are going wrong. Promptness is particularly important because of the preference in microservices architecture for service “choreography” and event-driven interactions that can lead to unexpected behavior of the system (emergent behavior). 4.3 Evolutionary Design In microservices architecture, service decomposition is perceived as a tool by which appli- cation developers can control changes in their application without slowing down the rate at which changes promulgate to the end user. The emphasis is on allowing fast, frequent, and well- controlled software changes. To achieve these properties, microservices-based systems need to adhere to a core principle: the decomposition of an application into services needs to be based on the notion that services can be independently replaced and upgraded without affecting the other application services they interact with. This notion is taken even further in some microservices- based systems in which it is no longer expected that microservices will be modified over the long term to accommodate change but rather that they will be scrapped and replaced by entirely new ones whenever their performance is no longer satisfactory. This emphasis on replaceability is a special case of modular design, which drives modularity through the pattern of change. The microservices-based application development team will need to monitor the software changes they perform to identify and refine how the application should be decomposed. The parts of the system that rarely change should be in a different module (microservice) than the parts of the systems that change often. Also, if two modules need to be changed together repeatedly, they should be merged into a single module. This decomposition and modulation process adds opportunities for more granular software release planning. With traditional monolithic systems, any change requires a full build and deployment of the entire application. With microservices, however, only services that are modified need to be redeployed, which simplifies and speeds up the release process. With more frequent changes to services also comes a certain level of complexity. The software development team will need to anticipate how changes to one service may affect other services and the end users. Traditionally, this has been reconciled through services versioning; however, with microservices, which emphasize tolerant and adaptive services, each service should be able to identify and adapt to other relevant services that may change.

Next: Section 5 - Design and Implementation for Sustainable DOT EIPs »
Guidance for Development and Management of Sustainable Enterprise Information Portals Get This Book
×
MyNAP members save 10% online.
Login or Register to save!
Download Free PDF

TRB's National Cooperative Highway Research Program (NCHRP) Research Report 865: Guidance for Development and Management of Sustainable Enterprise Information Portals provides guidance for the development and management of effective Enterprise Information Portals (EIPs) at state departments of transportation. EIPs have become key tools for transportation agencies as they make available information about the transportation system and the agency’s activities. Such EIPs must be curated; that is, there are people responsible for establishing the portal architecture, ensuring the quality of information and data, and maintaining the reliability of access. The report is intended to enhance agency personnel’s understanding of the value, uses, design, and maintenance of EIPs, and the design principles, management practices, and performance characteristics that will ensure that a DOT’s EIPs effectively and sustainably serve its users and the agency’s mission.

NCHRP Web-Only Document 241: Development and Management of Sustainable Enterprise Information Portals as well as a PowerPoint presentation on enterprise information portals (EIPs) for transportation agencies supplements the report. Use case diagrams referenced in the report are available in Visio format through a zip file.

This software is offered as is, without warranty or promise of support of any kind either expressed or implied. Under no circumstance will the National Academy of Sciences, Engineering, and Medicine or the Transportation Research Board (collectively "TRB") be liable for any loss or damage caused by the installation or operation of this product. TRB makes no representation or warranty of any kind, expressed or implied, in fact or in law, including without limitation, the warranty of merchantability or the warranty of fitness for a particular purpose, and shall not in any case be liable for any consequential or special damages.

  1. ×

    Welcome to OpenBook!

    You're looking at OpenBook, NAP.edu's online reading room since 1999. Based on feedback from you, our users, we've made some improvements that make it easier than ever to read thousands of publications on our website.

    Do you want to take a quick tour of the OpenBook's features?

    No Thanks Take a Tour »
  2. ×

    Show this book's table of contents, where you can jump to any chapter by name.

    « Back Next »
  3. ×

    ...or use these buttons to go back to the previous chapter or skip to the next one.

    « Back Next »
  4. ×

    Jump up to the previous page or down to the next one. Also, you can type in a page number and press Enter to go directly to that page in the book.

    « Back Next »
  5. ×

    To search the entire text of this book, type in your search term here and press Enter.

    « Back Next »
  6. ×

    Share a link to this book page on your preferred social network or via email.

    « Back Next »
  7. ×

    View our suggested citation for this chapter.

    « Back Next »
  8. ×

    Ready to take your reading offline? Click here to buy this book in print or download it as a free PDF, if available.

    « Back Next »
Stay Connected!