36,59 €
Design and develop high-performance, reusable, and maintainable applications using traditional and modern Julia patterns with this comprehensive guide
Key Features
Book Description
Design patterns are fundamental techniques for developing reusable and maintainable code. They provide a set of proven solutions that allow developers to solve problems in software development quickly. This book will demonstrate how to leverage design patterns with real-world applications.
Starting with an overview of design patterns and best practices in application design, you'll learn about some of the most fundamental Julia features such as modules, data types, functions/interfaces, and metaprogramming. You'll then get to grips with the modern Julia design patterns for building large-scale applications with a focus on performance, reusability, robustness, and maintainability. The book also covers anti-patterns and how to avoid common mistakes and pitfalls in development. You'll see how traditional object-oriented patterns can be implemented differently and more effectively in Julia. Finally, you'll explore various use cases and examples, such as how expert Julia developers use design patterns in their open source packages.
By the end of this Julia programming book, you'll have learned methods to improve software design, extensibility, and reusability, and be able to use design patterns efficiently to overcome common challenges in software development.
What you will learn
Who this book is for
This book is for beginner to intermediate-level Julia programmers who want to enhance their skills in designing and developing large-scale applications.
Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:
Seitenzahl: 569
Veröffentlichungsjahr: 2020
Copyright © 2020 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing or its dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
Commissioning Editor: Richa TripathiAcquisition Editor: Karan GuptaContent Development Editor: Tiksha SarangSenior Editor: Afshaan KhanTechnical Editor: Ketan KambleCopy Editor: Safis EditingProject Coordinator: Francy PuthiryProofreader: Safis EditingIndexer: Rekha NairProduction Designer: Nilesh Mohite
First published: January 2020
Production reference: 1170120
Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK.
ISBN 978-1-83864-881-7
www.packt.com
Packt.com
Subscribe to our online digital library for full access to over 7,000 books and videos, as well as industry leading tools to help you plan your personal development and advance your career. For more information, please visit our website.
Spend less time learning and more time coding with practical eBooks and Videos from over 4,000 industry professionals
Improve your learning with Skill Plans built especially for you
Get a free eBook or video every month
Fully searchable for easy access to vital information
Copy and paste, print, and bookmark content
Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.packt.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at [email protected] for more details.
At www.packt.com, you can also read a collection of free technical articles, sign up for a range of free newsletters, and receive exclusive discounts and offers on Packt books and eBooks.
Design patterns are the negative space of a programming language. They are the techniques that programmers come up with to effectively leverage a language's strengths and compensate for its weaknesses. We all use them whether we mean to or not. The classic Gang of Four Design Patterns book took existing patterns that were already being used in the wild and catalogued and classified them. Perhaps even more importantly, it gave them names so that programmers could refer to common patterns easily and immediately understand each other. It gave programmers a lingua franca for the tools of their trade.
Whereas classic design pattern books have focused almost exclusively on patterns themselves, assuming language proficiency as a given, Hands-on Design Patterns and Best Practices with Julia weaves together both the positive and negative space images of Julia. It introduces the language features that patterns depend on as they are used, making the book accessible even for readers who are not already fluent Julia programmers. This approach provides a comprehensive introduction of the language, while also covering advanced subjects as the book progresses. The later chapters delve into the kinds of sophisticated design patterns used by Julia wizards, so by the time you get to the end, you will truly have mastered the language. Be forewarned, however, as with most of the best programming books, it may require more than one read through before you've fully digested the content.
One of the more interesting aspects of creating a widely used programming language is seeing the remarkable and surprising things that people do with it. This includes incredible and sometimes world-changing applications that people have built in Julia—from specifying the FAA's next generation air collision avoidance system, to mapping all the visible universe's celestial bodies, to modeling climate change with unprecedented accuracy and resolution. But it also includes the clever programming tricks that people come up with to make it do their bidding. One of my favorites is the (Tim) Holy Trait Trick, discussed in Chapter 5, Reusability Patterns, which leverages the fact that Julia can efficiently dispatch on as many arguments as we want, to work around the language's lack of multiple inheritance. Not only does this technique get the job done, it goes well beyond: traits can depend on computed properties of types, allowing them to express relationships that multiple inheritance cannot. It turns out that the language already had the expressive power that was needed, it just took a clever design pattern to unlock it.
Tom's background gives him an expertly nuanced and balanced perspective on programming languages and their design patterns. He started programming in BASIC. But since those early days, he's used a broad variety of languages in professional settings, including: C++, Java, Python, TypeScript, Scheme and—of course—Julia. The set of technological sectors he's applied these languages in are equally diverse: finance, search engine, e-commerce, content management, and currently asset management. Perhaps not coincidentally, Julia is gaining significant traction in many of these sectors, especially those which are computationally demanding. Our backgrounds shape how we see the world and sometimes you find a new tool that feels like it was made for you. Sometimes you encounter a new programming language and think This is how I've always wanted to write programs! Julia has been that language for Tom and for many others. Hopefully it will be for you as well. Whether you are just trying Julia for the first time, or have used it for years and want to level up with more advanced techniques, you will find what you're looking for in this book. Enjoy and happy coding!
Stefan Karpinski
Co-creator of the Julia programming language
Co-founder of Julia Computing, Inc.
Tom Kwong, CFA, is an experienced software engineer with over 25 years of industry programming experience. He has spent the majority of his career in the financial services industry. His expertise includes software architecture, design, and the development of trading/risk systems. Since 2017, he has uncovered the Julia language and has worked on several open source packages, including SASLib.jl. He currently works at Western Asset Management Company, a prestige asset management company that specializes in fixed income investment services. He holds an MS degree in computer science from the University of California, Santa Barbara (from 1993), and he holds the Chartered Financial Analyst® designation since 2009.
Zhuo Qingliang (also known as KDR 2 online) is presently working at paodingai.com, which is a start up FinTech company in China that is dedicated to improving the financial industry by using artificial intelligence technologies. He has over 10 years of experience in Linux, C, C++, Python, Perl, and Java development. He is interested in programming, doing consulting work, and participating in, and contributing to, the open source community (including the Julia community, of course).
If you're interested in becoming an author for Packt, please visit authors.packtpub.com and apply today. We have worked with thousands of developers and tech professionals, just like you, to help them share their insight with the global tech community. You can make a general application, apply for a specific hot topic that we are recruiting an author for, or submit your own idea.
Title Page
Copyright and Credits
Hands-On Design Patterns and Best Practices with Julia
Dedication
About Packt
Why subscribe?
Foreword
Contributors
About the author
About the reviewer
Packt is searching for authors like you
Preface
Who this book is for
What this book covers
To get the most out of this book
Download the example code files
Code in Action
Conventions used
Get in touch
Reviews
Section 1: Getting Started with Design Patterns
Design Patterns and Related Principles
The history of design patterns
The rise of design patterns
More thoughts about GoF patterns
How do we describe patterns in this book?
Software design principles
SOLID
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
DRY
KISS
POLA
YAGNI
POLP
Software quality objectives
Reusability
Characteristics of reusable components
Performance
Characteristics of high-performance code
Maintainability
Characteristics of maintainable code
Safety
Characteristics of safe applications
Summary
Questions
Section 2: Julia Fundamentals
Modules, Packages, and Data Type Concepts
Technical requirements
The growing pains of developing applications
Data science projects
Enterprise applications
Adapting to growth
Working with namespaces, modules, and packages
Understanding namespaces
Creating modules and packages
Defining functional behavior
Exporting functions
Resolving conflicts
Creating submodules
Organizing files in a module
Managing package dependencies
Understanding the semantic versioning scheme
Specifying dependencies for Julia packages
Avoiding circular dependencies
What's the problem?
How do we fix this?
Designing abstract and concrete types
Designing abstract types
A personal asset type hierarchy example
Navigating the type hierarchy
Defining functions for abstract types
Descriptive functions
Functional behavior
Interaction between objects
Designing concrete types
Designing composite types
Immutability
Mutability
Mutable or immutable?
Supporting multiple types using Union types
Working with type operators
The isa operator
The <: operator
Differences between abstract and concrete types
Working with parametric types
Working with parametric composite types
Working with parametric abstract types
Conversion between data types
Performing simple data type conversion
Beware of lossy conversions
Understanding numeric type conversions
Reviewing the rules for automatic conversion
Case 1: Assigning a value to an array
Case 2: Assigning a value to a field of an object
Case 3: Constructing an object with the new function 
Case 4: Assigning to a variable that has a declared type
Case 5: Function has a declared return type
Case 6: Passing a value to ccall
Understanding the rules for function dispatches
Summary
Questions
Designing Functions and Interfaces
Technical requirements
Designing functions
Our use case – a space war game
Defining functions
Annotating function arguments
Untyped arguments
Typed arguments
Working with optional arguments
Utilizing keyword arguments
Accepting variable numbers of arguments
Splatting arguments 
Understanding first-class functions
Developing anonymous functions
Using do-syntax
Understanding Multiple Dispatch
What is a dispatch?
Matching to the narrowest types
Dispatching with multiple arguments
Possible ambiguities during dispatch 
Detecting ambiguities
Understanding dynamic dispatch
Leveraging parametric methods
Using type parameters
Replacing abstract types with type parameters
Enforcing type consistency in using parameters 
Extracting type information from the method signature
Working with interfaces
Designing and developing interfaces
Defining the Vehicle interface
Implementing FighterJet 
Handling soft contracts
Using interface traits
Summary
Questions
Macros and Metaprogramming Techniques
Technical requirements
Understanding the need for metaprogramming
Measuring performance with the @time macro
Unrolling loops
Working with expressions
Experimenting with the parser
Single-variable expressions
Function calls with keyword arguments
Nested functions
Constructing expression objects manually
Playing with more complex expressions
Assignment
Code blocks
Conditional
Loop
Function definition
Evaluating expressions
Interpolating variables in expressions
Using QuoteNode for symbols
Interpolating in nested expressions
Developing macros
What are macros?
Writing our first macro
Passing literal arguments
Passing expression arguments
Understanding the macro expansion process
Timing of macro expansion
Manipulating expressions
Example 1 – Making a new expression
Example 2 - Tweaking the abstract syntax tree
Understanding macro hygiene
Developing nonstandard string literals
Using generated functions
Defining generated functions
Examining generated function arguments
Summary
Questions
Section 3: Implementing Design Patterns
Reusability Patterns
Technical requirements
The delegation pattern
Applying the delegation pattern to a banking use case
Composing a new type that contains an existing type
Reducing boilerplate code for forwarding methods
Reviewing some real-life examples
Example 1 – the OffsetArrays.jl package
Example 2 – the ScikitLearn.jl package
Considerations
The holy traits pattern
Revisiting the personal asset management use case
Implementing the holy traits pattern
Defining the trait type
Identifying traits
Implementing trait behavior
Using traits with a different type of hierarchy
Reviewing some common usages
Example 1 – Base.IteratorSize
Example 2 – AbstractPlotting.jl ConversionTrait
Using the SimpleTraits.jl package
The parametric type pattern
Utilizing remove text parametric type for the stock trading app
Designing parametric types
Designing parametric methods
Using multiple parametric type arguments
Real-life examples
Example 1 – the ColorTypes.jl package
Example 2 – the NamedDims.jl package
Summary
Questions
Performance Patterns
Technical requirements
The global constant pattern
Benchmarking performance with global variables
Enjoying the speed of global constants
Annotating variables with type information
Understanding why constants help performance
Passing global variables as function arguments
Hiding a variable inside a global constant 
Turning to some real-life examples
Example 1 – SASLib.jl package
Example 2 – PyCall.jl package
Considerations
The struct of arrays pattern
Working with a business domain model
Improving performance using a different data layout
Constructing a struct of arrays
Using the StructArrays package
Understanding the space versus time trade-off
Handling nested object structures
Considerations
The shared array pattern
Introducing a risk management use case
Preparing data for the example
Overview of a high-performance solution
Populating data in the shared array
Analyzing data directly on a shared array
Understanding the overhead of parallel processing
Configuring system settings for shared memory usage
Adjusting system kernel parameters 
Configuring a shared memory device
Debugging the shared memory size issue 
Ensuring worker processes have access to code and data 
Avoiding race conditions among parallel processes
Working with the constraints of shared arrays
The memoization pattern
Introducing the Fibonacci function
Improving the performance of the Fibonacci function
Automating the construction of a memoization cache
Understanding the constraint with generic functions
Supporting functions that take multiple arguments
Handling mutable data types in the arguments
Memoizing generic functions with macros
Turning to real-life examples
Symata.jl
Omega.jl
Considerations
Utilizing the Caching.jl package
The barrier function pattern
Identifying type-unstable functions
Understanding performance impact
Developing barrier functions
Dealing with a type-unstable output variable
Using the @inferred macro
Summary
Questions
Maintainability Patterns
Technical requirements
Sub-module pattern
Understanding when sub-module is needed
Understanding afferent and efferent coupling
Organizing sub-modules
Referencing symbols and functions between modules and sub-modules
Referencing symbols defined in sub-modules
Referencing symbols from the parent module
Removing bidirectional coupling
Passing data as function arguments
Factoring common code as another sub-module
Considering splitting into top-level modules
Understanding the counterarguments of using sub-modules
Keyword definition pattern
Revisiting struct definitions and constructors
Using keyword arguments in constructors
Simplifying code with the @kwdef macro
Code generation pattern
Introducing the file logger use case
Code generation for function definitions
Debugging code generation
Considering options other than code generation
Domain-specific language pattern
Introducing the L-System
Designing DSL for L-System
Reviewing the L-System core logic
Developing the LModel object
Developing the state object
Implementing a DSL for L-System
Using the @capture macro 
Matching axiom and rule statements
Using the postwalk function
Developing the macro for a DSL
Summary
Questions
Robustness Patterns
Technical requirements
Accessor patterns
Recognizing the implicit interface of an object
Implementing getter functions
Implementing setter functions
Discouraging direct field access
Property patterns
Introducing the lazy file loader
Understanding the dot notation for field access
Implementing read access and lazy loading
Controlling write access to object fields
Reporting accessible fields
Let block patterns
Introducing the web crawler use case
Using closure to hide private variables and functions away
Limiting the variable scope for long scripts or functions
Exception handling patterns
Catching and handling exceptions
Dealing with various types of exceptions
Handling exceptions at the top level
Walking along the stack frames
Understanding the performance impact of exception handling
Retrying operations
Choosing nothing over exceptions
Summary
 Questions
Miscellaneous Patterns
Technical requirements
Singleton type dispatch pattern
Developing a command processor
Understanding singleton types
Using the Val parametric data type
Using singleton types with dynamic dispatch
Understanding the performance benefits of dispatch
Stubbing/Mocking pattern
What are testing doubles?
Introducing the credit approval use case
Performing state verification using stubs
Implementing stubs with the Mocking package
Applying multiple stubs to the same function
Performing behavior verification using mocks
Functional pipes pattern
Introducing the Hacker News analysis use case
Fetching top story IDs on Hacker News
Fetching details about a story
Calculating the average score for the top N stories
Understanding functional pipes
Designing composable functions
Developing a functional pipe for the average score function
Implementing conditional logic in functional pipes
Broadcasting along functional pipes
Considerations about using functional pipes
Summary
Questions
Anti-Patterns
Technical requirements
Piracy anti-pattern
Type I – Redefining a function
Type II piracy – Extending without your own types
Conflicting with another pirate
Future-proofing your code
Avoiding type piracy
Type III piracy – Extending with your own type, but for a different purpose
Narrow argument types anti-pattern
Considering various options for argument types
Option 1 – Vectors of Float64 values
Option 2 – Vectors of instances of Number
Option 3 – Vectors of type T where T is a subtype of Number
Option 4 – Vectors of type S and T where S and T are subtypes of Number
Option 5 – Arrays of type S and type T where S and T are subtypes of Number
Option 6 – Abstract arrays
Option 7 – Duck typing
Summarizing all options
Evaluating performance
Nonconcrete field types anti-pattern
Understanding the memory layout of composite data types
Designing composite types with concrete types in mind
Comparing performance between concrete versus nonconcrete field types
Summary
Questions
Traditional Object-Oriented Patterns
Technical requirements
Creational patterns
The factory method pattern
The abstract factory pattern
The singleton pattern
The builder pattern
The prototype pattern
Behavioral patterns
The chain-of-responsibility pattern
The mediator pattern
The memento pattern
The observer pattern
The state pattern
The strategy pattern
The template method pattern
Command, interpreter, iterator, and visitor patterns
Structural patterns
The adapter pattern
The composite pattern
The flyweight pattern
Bridge, decorator, and facade patterns
Summary
Questions
Section 4: Advanced Topics
Inheritance and Variance
Technical requirements
Implementing inheritance and behavior subtyping
Understanding implementation inheritance
Understanding behavior subtyping
The square-rectangle problem
The fragile base class problem
Revisiting duck typing
Covariance, invariance, and contravariance
Understanding different kinds of variance
Parametric types are invariant
Method arguments are covariant
Dissecting function types 
Determining the variance of the function type 
Implementing our own function type dispatch
Parametric methods revisited
Specifying type variables
Matching type variables
Understanding the diagonal rule
An exception to the diagonal rule
The availability of type variables
Summary
Questions
Assessments
Chapter 1 
Chapter 2
Chapter 3
Chapter 4
Chapter 5
Chapter 6
Chapter 7
Chapter 8
Chapter 9
Chapter 10
Chapter 11
Chapter 12
Other Books You May Enjoy
Leave a review - let other readers know what you think
Julia is a powerful programming language that is designed to enable high-performance applications with developer productivity in mind. Its dynamic nature allows you to quickly perform small-scale experiments and then migrate to larger applications. Its introspection tools allow us to optimize performance by analyzing how high-level code is translated into lower-level instructions and machine code. Its metaprogramming facility helps more advanced programmers to build custom syntax for their specific domain usage. Its multiple dispatch and generic function features make it easy to build new capabilities by extending existing functions. For these and many more reasons, Julia is an excellent tool for developing applications across a wide spectrum of industries.
This book fulfills several demands from Julia developers. A desire to write better code. A desire to improve system performance. A desire to design software that is easy to maintain. From the time that the Julia language was born to its magnificent milestone of version 1.0 in August 2018, many design patterns have already emerged from the brightest minds, ranging from the core developers of the language to heavy users of the language. At times, these patterns were presented in blog posts and conferences. Sometimes, they showed up in random discussion threads on the Julia Discourse forum. At other times, they come up in casual talks between community members on various Julia Slack channels. This book is a collection of patterns, documenting the best approaches to designing high-quality Julia applications.
The primary objective of this book is to organize these well-proven patterns into a format that is easily consumable by the Julia developer community. There are several benefits to organizing and naming these patterns:
It allows developers to communicate with each other more easily.
It allows developers to better understand code that uses these patterns.
It allows developers to articulate when a pattern should be applied.
The goal of this book is simple but powerful – after reading this book, you should be more knowledgeable about how to design and develop software in Julia. In addition, the material presented in this book can serve as a reference for any future discussions regarding design patterns in Julia. As we know from history, new design patterns will continue to emerge alongside the continuous evolution of the Julia language.
I hope you enjoy this book. Happy reading!
This book is for beginner-to-intermediate Julia developers who want to get better at writing idiomatic Julia code for larger applications. It is not an introductory book, so you are expected to have some basic programming knowledge. If you are familiar with the object-oriented programming paradigm, then you may find this book helpful where it shows how the same problem can be solved differently, and often in a better way, in Julia.
Many of the patterns described in this book are applicable to any industry domain and use cases. Whether you are a data scientist, researcher, system programmer, or enterprise application developer, you should be able to benefit from using these patterns in your projects.
Chapter 1, Design Patterns and Related Principles, introduces the history of design patterns and how they are useful for developing applications. It covers several industry-standard software design principles that are applicable across any programming language and paradigm.
Chapter 2, Modules, Packages, and Data Type Concepts, discusses how larger programs can be organized and how dependencies can be managed. Then, it explains how to develop new data types and express their relationship in a custom type hierarchy.
Chapter 3, Designing Functions and Interfaces, explains how functions are defined and how multiple dispatch come into play. It also discusses parametric method and interfaces, for which different functions can work with each other properly based on a pre-determined contract.
Chapter 4, Macros and Metaprogramming Techniques, introduces macro programming facility and how it can be used to transform source code into a different form. It describes several techniques for developing and debugging macros more effectively.
Chapter 5, Reusability Patterns, covers design patterns that relate to code reuse. This includes the Delegation pattern for reusing code via composition, the Holy Traits pattern for a more formal interface contract, and the Parametric Type pattern for creating new types from a parameterized data structure.
Chapter 6, Performance Patterns, covers design patterns that relate to improving system performance. This includes the Global Constant pattern for better type stability, the Memoization pattern for caching prior computation results, the Struct of Arrays pattern for rearranging data for a more optimal layout, the Shared Array pattern for optimizing memory usage with parallel computing, and the Barrier Function pattern for improving performance via function specialization.
Chapter 7, Maintainability Patterns, covers design patterns about code maintainability. This includes the Sub Module pattern for better organization of larger code bases, the Keyword Definition pattern for creating data types that can be constructed more easily, the Code Generation pattern for defining many similar functions with less code, and the Domain-Specific Language pattern for creating a new syntax for a specific domain.
Chapter 8, Robustness Patterns, covers design patterns that help you write safer code. This include the Accessor pattern for providing standard access to fields, the Property pattern for controlling access to fields, the Let-Block pattern for limiting the variable scope, and the Exception Handling pattern for handling errors.
Chapter 9, Miscellaneous Patterns, covers several design patterns that do not fit into the preceding categories. It includes the Singleton Type pattern for use with dynamic dispatch, the Mocking pattern for building isolated tests, and the Functional Pipe pattern for building linear data processing pipelines.
Chapter 10, Anti-Patterns, covers patterns that should be avoided. The main anti-pattern is Piracy, which involves defining or extending functions for data types that you do not own. Then, it covers the Narrow Arguments and Non-Concrete Type Fields patterns, which hinder system performance.
Chapter 11, Traditional Object-Oriented Patterns, covers the traditional object-oriented patterns described in the Design Patterns book by Gang-of-Four. It discusses how those patterns can be simplified or implemented differently in Julia.
Chapter 12, Inheritance and Variance, discusses how Julia supports inheritance and why it is designed as such since its approach is quite different from mainstream object-oriented programming languages. Then, it covers the topic of type variance, an important concept in terms of subtyping relationships between data types used by multiple dispatch.
You should download the latest version of Julia from the Julia Language website (https://julialang.org/).
The code samples are available on GitHub, as described in the Technical requirements section of each chapter. At the time of writing, the code has been tested with Julia version 1.3.0. To download the code samples, clone the project from GitHub as follows:
You are encouraged to run and experiment with the code samples accompanying this book. The code samples are typically stored in one of the following formats:
Code snippets in a Julia source file. These snippets can be copied and pasted into the REPL.
Code residing in a package directory. The package can be instantiated as follows:
For example, in Chapter 5, Reusability Patterns, the content is listed as follows:
To use the code for DelegationPattern, just start a Julia REPL in that folder with the --project=. command-line argument:
Then, go to the package mode and instantiate the package by entering the ] instantiate command:
After that, you can use the package as usual:
If there is a test directory, then you can read and run the test scripts provided.
You can download the example code files for this book from your account at www.packt.com. If you purchased this book elsewhere, you can visit www.packtpub.com/support and register to have the files emailed directly to you.
You can download the code files by following these steps:
Log in or register at
www.packt.com
.
Select the
Support
tab.
Click on
Code Downloads
.
Enter the name of the book in the
Search
box and follow the onscreen instructions.
Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of:
WinRAR/7-Zip for Windows
Zipeg/iZip/UnRarX for Mac
7-Zip/PeaZip for Linux
The code bundle for the book is also hosted on GitHub at https://github.com/PacktPublishing/Hands-on-Design-Patterns-and-Best-Practices-with-Julia. In case there's an update to the code, it will be updated on the existing GitHub repository.
We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!
Visit the following link for the Code in Action videos:
http://bit.ly/36Z4oXs
There are a number of text conventions used throughout this book.
CodeInText: Indicates code words in text such as variable names, function names, data types, etc. For example, "The format function takes a formatter and a numeric value, x, and returns a formatted string."
A block of code is set as follows:
abstract type Formatter endstruct IntegerFormatter <: Formatter endstruct FloatFormatter <: Formatter end
Any experiment or output from the REPL is presented as screenshots:
Bold: Indicates an important word or concept. For example, "The Bridge pattern is used to decouple an abstraction from its implementation so that it can evolve independently."
Italics: Emphasizes a new concept that will be explained later in the text. For example, "The cases presented in previous chapters include various situations that we can solve by writing idiomatic Julia code."
Feedback from our readers is always welcome.
General feedback: If you have questions about any aspect of this book, mention the book title in the subject of your message and email us at [email protected].
Errata: Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you have found a mistake in this book, we would be grateful if you would report this to us. Please visit www.packtpub.com/support/errata, selecting your book, clicking on the Errata Submission Form link, and entering the details.
Piracy: If you come across any illegal copies of our works in any form on the Internet, we would be grateful if you would provide us with the location address or website name. Please contact us at [email protected] with a link to the material.
If you are interested in becoming an author: If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, please visit authors.packtpub.com.
Please leave a review. Once you have read and used this book, why not leave a review on the site that you purchased it from? Potential readers can then see and use your unbiased opinion to make purchase decisions, we at Packt can understand what you think about our products, and our authors can see your feedback on their book. Thank you!
For more information about Packt, please visit packt.com.
The aim of this section is to introduce you to how design patterns are used in general and how Julia is different from the object-oriented programming paradigm.
This section contains the following chapter:
Chapter 1
,
Design Patterns and Related Principles
Nowadays, learning and applying design patterns is an important aspect of software engineering. Design patterns are like water – you can't live without them. Don't believe me? Just ask hiring managers, and you will find that many of them have design patterns in their job postings as well as related questions in job interviews. It is a common belief that design patterns are important ingredients for software development and everyone should know them.
In this chapter, we will provide some context about why design patterns are useful and how they have served us well in the past few decades. By understanding the motivation behind design patterns, we will be able to set forth a set of guiding principles for developing software. The following topics will be discussed in this chapter:
The history of design patterns
Software design principles
Software quality objectives
Let's get started!
Design patterns is not a new concept to computer programmers. Since personal computers became more affordable and popular in the 1980s, the programming profession flourished and a lot of code was written for a variety of applications.
I remember that, when I was 14 years old, learning the GOTO statement for a BASIC program was one of the coolest things. It literally allowed me to take a control flow to a different part of the code at any time. Perhaps not too surprisingly, when I learned about structured programming and the Pascal language in college, I started to realize how GOTO statements produce messy spaghetti code. Using GOTO for branching purposes is a pattern. It's just a bad one because it makes code difficult to understand, follow, and debug. In today's lingua franca, we call them anti-patterns. When it comes to structured programming techniques, organizing code in small functions is a pattern as well, one that has been taught as a mainstream subject in programming courses.
When I graduated from college, I started my programming career and spent plenty of time hacking away. I had the opportunity to do various kinds of research and find out how systems are designed. For example, I realized that the Unix operating system has a beautiful design. That is because it consists of many small programs, which individually do not have a ton of functionality, but you can compose them in any number of ways to solve more complex problems. I was also fond of the Scheme programming language, which came out of MIT's AI Lab. The simplicity and versatility of the language still amazes me today. Scheme's heritage can be traced to Lisp, which had some influence on how the Julia language was designed.
In 1994, while I was diving deep into C++ and distributed computing for a financial application, four software professionals, also known as the Gang of Four or GoF, came together and published a book about design patterns, and it took the object-oriented programming community by storm. The group collected and classified 23 design patterns that were commonly utilized when developing large-scale systems. They also chose to explain the concepts using Unified Modeling Language (UML) and C++ and Smalltalk.
For the first time, a set of design patterns had been collected, organized, explained, and widely distributed to software developers. Perhaps one of the most significant decisions by the group was to organize these patterns in a highly structured and easily consumable format. Since then, programmers have been able to communicate with each other easily about how they design their software. In addition, they can visually present software design with a universal notation. When one person talks about the Singleton pattern, another person can immediately understand and even visualize in his/her mind how that component works. Isn't that convenient?
Even more surprisingly, design patterns suddenly became the gospel when it come to building good software. In some ways, using them was even perceived as the only way to write good software. GoF patterns were so widely preached across the development community that many people abused them and used them everywhere without good reason. The problem is – When all you have is a hammer, everything looks like a nail! Not everything can be solved or should be solved by the same patterns. When design patterns are overused or misused, the code becomes more abstract, more complicated, and more difficult to manage.
So, what have we learned from the past? We recognize that every abstraction comes with a cost. Every design pattern comes with its own pros and cons. One of the main objectives of this book is to discuss not just the how but also the why and why not, and under what circumstances a pattern should be used or not used. We, as software professionals, will then be equipped with the information we need to make good judgment calls about when to apply these patterns.
GoF design patterns are classified into three main categories:
Creational patterns
: These cover how to construct objects in various ways. Since object-oriented programming brings together data and behavior, and a class may inherit the structure and behavior of an ancestor class, there are some complexities involved when building a large application. Creational patterns help standardize object creation methods in various situations.
Structural patterns
:
These cover how objects can be extended or composed to make bigger thing. The purpose of these patterns is to allow software components to be reused or replaced more easily.
Behavioral patterns
:
This cover how objects can be designed to perform separate tasks and communicate with each other. A large application can be decomposed into independent components and the code becomes easier to maintain. The o
bject-oriented programming paradigm requires solid interaction between objects. The purpose of these patterns is to make software components more flexible and more convenient for collaboration with each other.
One school of thought is that design patterns are created to address limitations in their respective programming language. Two years after the GoF book was published, Peter Norvig published research showing that 16 of the 23 design patterns are either unnecessary or can be simplified in a dynamic programming language such as Lisp.
This is not an unimportant observation. In the context of object-oriented programming, additional abstraction from a class hierarchy requires the software designer to think about how objects are instantiated and interact with each other. In a strong, statically typed language such as Java, it is even more necessary to reason about the behavior and interaction of objects. In Chapter 11, Traditional Object-Oriented Patterns, we will circle back to this topic and discuss how Julia works differently compared to object-oriented programming.
For now, we will start with the basics and review some software design principles. These principles are like the North star, guiding us as we build applications.
If you are new to Julia programming, this book will help you understand how to write more idiomatic Julia code. We will also focus on describing some of the most useful patterns that are already used in the existing open source Julia ecosystem. That includes Julia's own Base andstdlibpackages as the Julia runtime is largely written in Julia itself. We will also reference other packages that are used for numerical computing and web programming.
For ease of reference, we will organize our patterns by name. For example, the Holy Traits pattern refers to a specific method for implementing traits. The Domain-Specific Language pattern talks about how to build new syntax to represent specific domain concepts. The sole purpose of having a name is just ease of reference.
When we discuss these design patterns in this book, we will try to understand the motivation behind them. What specific problem are we trying to solve? What would be a real-world situation where such a pattern would be useful? Then, we will get into the details of how to solve these problems. Sometimes, there may be several ways to solve the same problem, in which case we will look into each possible solution and discuss the pros and cons.
Having said that, it is important for us to understand the ultimate goal of using design patterns. Why do we want to use design patterns in the first place? To answer this question, it would be useful for us to first understand some key software design principles.
While this book does not cover object-oriented programming, some object-oriented design principles are universal and could be applied to any programming language and paradigm. Here, we will take a look at some of the most well-known design principles. In particular, we will cover the following:
SOLID
: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
DRY
: Don't Repeat Yourself
KISS
: Keep It Simple, Stupid!
POLA
: Principle of Least Astonishment
YAGNI
: You Aren't Gonna Need It
POLP
: Principle of Least Privilege
Let's start with SOLID.
The SOLID principle consists of the following:
S
: Single Responsibility Principle
O
: Open/Closed Principle
L
: Liskov Substitution Principle
I
: Interface Segregation Principle
D
: Dependency Inversion Principle
Let's understand each concept in detail.
The Single Responsibility Principle states that every module, class, and function should be responsible for a single functional objective. There should be only one reason to make any changes.
The benefits of this principle are listed here:
The programmer can focus on a single context during development.
The size of each component is smaller.
The code is easier to understand.
The code can be tested more easily.
The Open/Closed Principle states that every module should be open for extension but closed for modification. It is necessary to distinguish between enhancement and extension—enhancement refers to a core improvement of the existing module, while an extension is considered an add-on that provides additional functionality.
The following are the benefits of this principle:
Existing components can be easily reused to derive new functionalities.
Components are loosely coupled so it is easier to replace them without affecting the existing functionality.
The Liskov Substitution Principle states that a program that accepts type T can also accept type S (which is a subtype of T), without any change in behavior or intended outcome.
The following are the benefits of this principle:
A function can be reused for any subtype passed in the arguments.
The Interface Segregation Principle states that a client should not be forced to implement interfaces that it does not need to use.
The following are the benefits of this principle:
Software components are more modular and reusable.
New implementations can be created more easily.
The Dependency Inversion Principle states that high-level classes should not depend on low-level classes; instead, high-level classes should depend on an abstraction that low-level classes implement.
The following are the benefits of this principle:
Components are more decoupled.
The system becomes more flexible and can adapt to changes more easily.
Low-level components can be replaced without affecting high-level components.
We'll now cover the DRY principle:
D
: Don't
R
: Repeat
Y
: Yourself
This acronym is a good way of reminding programmers that duplicate code is bad. It is obvious that duplicate code can be difficult to maintain—whenever logic is changed, multiple places in the code are affected.
What do we do when duplicate code is found? Eliminate it and create a common function that is reusable from multiple source files.
In addition, sometimes code is not 100% duplicated but instead is 90% similar. That is not an uncommon scenario. In that case, consider redesigning the relevant components, possibly refactoring code to a common interface.
Let's talk about the KISS principle:
K
: Keep
I
: It
S
: Simple
S
: Stupid!
Often, when we design software, we like to think ahead and try to deal with all kinds of future scenarios. The trouble with building such future-proof software is that it takes exponentially more effort to design and code properly. Practically speaking, it's a conundrum—there is no 100% future-proof solution because technology changes, business changes, and people change. Also, over-engineering could lead to excessive abstraction and indirection, making a system more difficult to test and maintain.
In addition, when using Agile software development methods, we value faster and high-quality delivery over perfection or excess engineering. Keeping the design and code simple is a virtue that every programmer should keep in mind.
Let's look at the POLA principle:
P
: Principle
O
: Of
L
: Least
A
: Astonishment
POLA states that a software component should be easy to understand and its behavior should never be a surprise (or, more accurately, astonishing) to the client. How do we do that?
The following are some things to keep in mind:
Make sure that the names of the module, function, or function arguments are clear and unambiguous.
Ensure that modules are right-sized and well maintained.
Ensure that interfaces are small and easy to understand.
Ensure that functions have few positional arguments.
Let's move on to the YAGNI principle:
Y
: You
A
: Aren't
G
: Gonna
N
: Need
I
: It
YAGNI says you should only develop software that is needed today. This principle came from Extreme Programming (XP). See what Ron Jeffries, co-founder of XP, wrote in his blog:
Software engineers are sometimes tempted to develop functionality that they feel the customer will need in the future. It's been proven time and time again that this is not the most effective way to develop software. Consider the following scenarios:
The functionality is never needed by the customer and so the code is never used.
The business environment changes and the system has to be redesigned or replaced.
The technology changes and the system has to be upgraded to use a new library, a new framework, or a new language.
The cheapest software is the one that you didn't write. You aren't gonna need it!
Now, for POLP:
P
: Principle
O
: Of
L
: Least
P
: Privilege
POLP states that a client must be given access only to the information or functions that they need. POLP is one of the most important pillars for building secure applications, and it is widely adopted by cloud infrastructure vendors such as Amazon, Microsoft, and Google.
There are quite a few benefits when POLP is applied:
Sensitive data is protected and not exposed to non-privileged users.
The system can be tested more easily since the number of use cases is limited.
The system becomes less prone to misuse because only limited access is given and the interface is simpler.
The software design principles that we have learned about so far are great tools. Although SOLID, DRY, KISS, POLA, YAGNI, and POLP seem to be just a bunch of acronyms, they are useful in designing better software. While SOLID principles came from the object-oriented programming paradigm, SOLID's concepts can still be applied to other languages and environments. As we work through the rest of the chapters in this book, I would encourage you to keep them in mind.
In the next section, we will go over several software quality objectives when designing software.
Everyone likes beautiful design. I do, too. But, the use of design patterns is not just to make something look good. Everything we do should have a purpose.
The GoF classified object-oriented design patterns as creational, structural, and behavioral. For Julia, let's take a different perspective and classify our patterns by their respective software quality objectives as follows:
Reusability
Performance
Maintenance
Safety
Let's understand each of these in the following sections.
People often talk about top-down and bottom-up approaches when designing software.
The top-down approach starts with a large problem and breaks it down into a set of smaller problems. Then, if the problems are not small enough, as discussed when we looked at the Single Responsibility Principle, we further break down the problem into even smaller ones. The process repeats and eventually the problem is small enough to design and code.
The bottom-up approach works in the opposite direction. Given domain knowledge, you can start creating building blocks, and then create more complex ones by composing from these building blocks.
Regardless of how it is done, eventually there will be a set of components that work with each other, thereby forming the basis of the application.
I like the metaphor. Even a 5-year old child can build a variety of structures using just several kinds of Lego block. Imagination is the limit. Do you ever wonder why it is so powerful? Well, if you recall, every Lego block has a standard set of connectors: one, two, four, six, eight, or more. Using these connectors, each block can plug into another block easily. When you create a new structure, you can combine it with other structures to create even larger, more complex structures.
When building applications, the key design principle is to create pluggable interfaces so every component can be reused easily.
The following are important characteristics of reusable components:
Each component serves a single purpose (the S in SOLID).
Each component is well defined and ready for reuse (the O in SOLID).
An abstract type hierarchy is designed for parent-child relationships (the L in SOLID).
Interfaces are defined as a small set of functions (the I in SOLID).
Interfaces are used to bridge between components (the D in SOLID).
Modules and functions are designed with simplicity in mind (KISS).
Reusability is important because it means we can avoid duplicated code and wasted effort. The less code we write, the less work we need to do to maintain software. That includes not just the development effort but also the time testing, packaging, and upgrading. Reusability is also one of the reasons why open source software is so successful. In particular, the Julia ecosystem contains many open source packages and they tend to borrow functionalities from each other.
Next, we will discuss another software quality objective—performance.
The Julia language is designed for high-performance computing. It does not come for free, however. When it comes to performance, it takes practice to write code that is more compiler-friendly, thus making it more likely to translate the program into optimized machine code.
For the past few decades, computers have seemed to become faster and faster every year. What used to be performance bottlenecks are more easily solved using today's hardware. At the same time, we are also facing more challenges due to the explosion of data. A good example is the field of big data and data science. As the amount of data grows, we need even more computing power to handle these new use cases.
Unfortunately, the speed of computers has not grown as rapidly as it did in the past. Moore's Law states that the number of transistors on a microchip doubles roughly every 18 months, and since 1960 it has been correlated with the growth in CPU speed. However, it is well known that Moore's Law will no longer be applicable soon due to a physical limitation: the number of transistors that can be fitted to a chip and the precision of the fabrication process.
In order to address today's computational needs, especially in the world of artificial intelligence, machine learning, and data science, practitioners have been gearing toward a scale-out strategy that utilizes multiple CPU cores across many servers, and looking at exploiting the efficiency of GPUs and TPUs.
The following are characteristics of high-performance code:
Functions are small and can be optimized easily (S in SOLID).
Functions contains simple logic rather than complex logic (KISS).
Numeric data is laid out in contiguous memory space so the compiler can fully utilize CPU hardware.
Memory allocation should be kept to a minimum to avoid excessive garbage collection.
Performance is an important aspect of any software project. It is particularly important for data science, machine learning, and scientific computing use cases. A small design change can make a big difference—depending on the situation, it could possibly turn a 24-hour process into a 30-minute process. It could also give users real-time experience when using a web application rather than a please wait... dialog.
Next, we will discuss software maintainability as another software quality objective.
Software can be maintained more easily when it is designed properly. Generally speaking, if you are able to effectively use the design principles listed previously (SOLID, KISS, DRY, POLA, YAGNI, and POLP), then your application is more likely to be well architected and designed for long-term maintenance.
Maintainability is an important ingredient for large-scale applications. A research project from graduate school may not last long. On the contrary, an enterprise application may last for decades. Recently, I heard from a colleague that COBOL is still in use and COBOL programmers are still making a good living.
We often hear about technical debt. Similar to monetary debt in real life, technical debt is something that you must pay for whenever code is changed. And the longer the technical debt stays in place, the more effort you have to spend.
To understand why, consider a module that is bloated with duplicate code or unnecessary dependencies. Whenever a new functionality is added, you have to update multiple parts of the source code, and you have to perform regression testing for a larger area of the system. So, you end up paying (in terms of programming time and effort) for the debt every time the code is changed until the debt is fully repaid (that is, until the code is fully refactored).
The following are characteristics of maintainable code:
No unused code (YAGNI).
No duplicate code (DRY).
Code is concise and short (KISS).
Code is clear and easy to understand (KISS).
Every function has a single purpose (the S in SOLID).
Every module contains functions that relate to and work with each other (the S in SOLID).
Maintainability is an important aspect of any application. When designed properly, even large applications can be changed frequently and easily without fear. Applications can also last a long time, reducing the cost of the software.
Next, we will discuss software safety as another quality objective.
Applications are expected to function correctly. When an application malfunctions, there could be undesired consequences and some of those could be fatal. Consider a mission-critical rocket-launch subsystem used by NASA. A single defect could cause the launch to be delayed; or, in the worst-case scenario, it could cause the rocket to explode in mid-air.
Programming languages are designed to allow flexibility but at the same time provide safety features so software engineers can make fewer mistakes. For example, the compiler's static type checking ensures that the correct types are passed to functions that expect those types. In addition, most computer programs operate on data, and as we know, data is not always clean or available. Hence, the ability to handle bad or missing data is an important software quality.
Some characteristics of safe applications follow:
Each module exposes a minimum set of types, functions, and variables.
Each function is called with arguments such that the respective types implement the expected behavior of the function (the L in SOLID; POLA).
The return value of a function is clear and documented (POLA).
Missing data is handled properly (POLA).
Variables are limited to the smallest scope.
Exceptions are caught and handled accordingly.
Safety is one of the most important objectives here. An erroneous application can cause major disasters. It can even cost a company millions of dollars. In 2010, Toyota recalled over 400,000 of its Prius hybrid cars due to a software defect with the Anti-lock Braking System (ABS). In 1996, the Ariane 5 rocket launched by the European Space Agency exploded just 40 seconds after launch. Of course, these are only a few more extreme examples. By utilizing best practices, we can avoid getting into these kinds of embarrassing and costly incidents.
Now, we understand the importance of software design principles and software quality objectives.
In this chapter, we started by going back in time and reviewing the history of design patterns. We discussed why design patterns can be useful for software professionals and how we would like to organize design patterns in this book given what we have learned in the past.
