Software design patterns in Kotlin
Creational patterns encapsulate knowledge about concrete classes the system use, hide how instances of these classes are created and separate the system from how the objects are composed and represented.
-
Abstract factory (usage)
What. Abstract Factory provides an interface for creating families of related/dependent objects without specifying their concrete classes. Abstract Factory abstracts and encapsulates the creation of a suite of products for a given platform/family that the system depends on
How.AbstractFactory
interface provides methods for creating all kinds of products for a given platform/family. Client works only with theAbstractFactory
and theProduct
interfaces. The concrete implementations of theAbstractFactory
interface are singletons
Example. The system depends on aLetter
=Product
and aResume
=Product
instances. The system uses aDocumentCreator
=AbstractFactory
for creating the concreteLetter
andResume
instances from the two families: modern and fancy -
Builder (usage) #
What. Builder separates the construction of a complex object from its representation, allowing the same step by step construction process to create various representations
How. Client uses (a) a separateBuilder
object which receives each initialization parameter step by step in a fluent interface or (b) aBuilder
DSL initialization method which configures properties by direct property assignment or function call and returns the resulting constructed complex object at once
Example.Car.Builder
provides a builder DSL (Car.build { ... }
) for building aCar
instance -
Dependency injection (usage) #
What. A class (client) accepts through an interface objects (services/dependencies) the class requires from an injector instead of creating the objects directly
How. TheInjector
passes theService
object to theClient
class via theService
interface by the inversion of control.Injector
decouples theClient
from creating theService
object directly. TheInjector
creates theService
object and calls theClient
. TheClient
does not know about theInjector
. TheClient
works only with theService
interfaces provided by theInjector
via constructor injection or setter injection
Example. TheClient
requires theConstructorInjectedDependency
and theSetterInjectedDependency
both implementing theService
interface. TheInjector
creates theClient
and sets up its dependentServices
via the constructor injection and the setter injection -
Factory method (usage)
What. Factory method defines an interface for creating a single object, but let subclasses decide which class to instantiate
How.FactoryMethod
interface provides a method for creating a single object. Client works only with theFactoryMethod
and theProduct
interfaces. The concrete implementations of theFactoryMethod
interface are singletons
Example. The system depends on anArticle
=Product
instances. The system usesArticleCreator
=FactoryMethod
for creating a singleArticle
instance from the two families: modern and fancy -
Prototype (usage) #
What. Prototype creates new objects by cloning prototypical instance, boosting performance and keeping memory footprint to a minimum
How. Client works only with theProduct
interface and uses theProduct::clone()
method for creating new instances of theProduct
Example. TheProkaryoteCell
=Product
and theEukaryoteCell
=Product
implement theCellPrototype
interface. In order to create a new instance of the specific cell theCellPrototype::clone()
method is used -
Singleton (usage)
What. Singleton ensures that a class has only one instance and provides a global point of access to the instance. Singleton global variable like dependency is not declared in any component interface, but is tightly coupled with all component implementations. Replace Singleton pattern with Dependency Injection pattern based on Dependency Inversion Principle
How. Make the class constructor private and provide one public static method that always returns the same single instance of the class stored in a private static variable
Example. The expressionobject Singleton
defines a Singleton class and immediately instantiate the class with the single point of access to the instance being theSingleton
class name
Structural patterns provide simple way of implementing relationships between objects.
-
Adapter (usage)
What. Adapter converts the interface of the class without modifying class code into another interface that client expects
How. Client works with the class through an implementation of theAdapter
interface that client expects and delegation to the class methods
Example. Client expects thePhone
=Adapter
interface.XiaomiPhoneAdapter
implements thePhone
interface that client expects and delegates to theXiaomiPhone
class methods -
Bridge (usage)
What. Bridge decouples an abstraction from its implementation (two orthogonal dimensions) allowing the two to vary independently
How. TheAbstraction
and itsImplementation
are defined and extended independently. TheAbstraction
is implemented by delegating to itsImplementation
Example. TheDevice
=Abstraction
has two implementationsPhoneDevice
andTabletDevice
. TheVendor
=Implementation
has two implementationsXiaomiVendor
andNokiaVendor
. TheDevice
implementations acceptVendor
implementation. TheDevice::switchOn()
method delegates to theVendor::support(Device)
method -
Composite (usage) #
What. Composite treats individual objects and composition of objects uniformly. Composes objects into tree structures to represent part-whole hierarchies
How. TheLeaf
and theComposite
classes implement theComponent
interface. TheLeaf
class implements the request directly while theComposite
class forwards recursively the request to composite's children
Example. TheExpression
=Component
is a uniformComponent
interface. TheOperand
=Leaf
implements the request directly by returning the operand value. TheOperation
=Composite
implements the request recursively by evaluating its right and left expressions and than applying the actual operation to the results of the left and right expression evaluations -
Decorator (usage) #
What. Decorator attaches additional behavior to an individual object dynamically keeping the same interface without affecting the behavior of other objects of the same class
How. TheDecorator
class implements the originalComponent
interface by delegating the request to the original object and adding behavior before/after the original request. Multiple decorators can be stacked on top of each other
Example. TheSimpleCoffee
original class implements theCoffee
=Component
interface. TheCoffeeWithSugar
=Decorator
and theCoffeeWithMilk
=Decorator
decorators implement theCoffee
interface and accept theCoffee
instance to delegate to -
Facade (usage)
What. Facade defines a higher-level simplified interface that makes a system/library easier to use. Facade hides the complexities of a larger system with dependencies and provides a simpler interface to the client
How. Client works only with theFacade
higher-level simplified interface to interact with the larger system
Example. The larger systemDesktop
implements theComputer
=Facade
interface which is used by the client. TheDesktop
manages internally all the complexities involved with the subsystemsCpu
,Ram
andSsd
-
Flyweight (usage)
What. Flyweight uses sharing to support a large number of similar objects efficiently
How. Shares theInvariant
/intrinsic object state in an external data structure. When a new object is created theFlyweightFactory
provides the cachedInvariant
/intrinsic object state and allows theVariant
/extrinsic object state to be set through theFlyweight
interface
Example. TheGlyphCode
=Invariant
represents the intrinsic glyph state (code) that can be cached and shared between glyphs. TheGlyphFlyweight
=Invariant
+Variant
class implements theGlyph
=Flyweight
interface that allows extrinsic glyph state (position) modification. TheGlyphFactory
=FlyweightFactory
caches and shares efficiently theGlyphCode
instances -
Proxy (usage) #
What. Proxy provides a placeholder/wrapper for another object for access control, request validation, response caching, etc.
How. The real object and theProxy
implement the sameSubject
interface, so the client cannot distinguish between the real object and theProxy
. TheProxy
uses delegation to the real object
Example. The realAccount
object implements thePayment
=Subject
interface without any balance/amount validations. ThePaymentProxy
=Proxy
implements thePayment
interface with the balance/amount validation. Client uses only theSubject
interface to work with both the realAccount
object or with thePaymentProxy
proxy object
Behavioral patterns provide simple way of implementing interactions between objects.
-
Chain of responsibility (usage) #
What. Chain of Responsibility chains the receiving objects/functions (handlers) and pass the request along the chain until an object/function handles the request completely or partially. Avoids coupling of the request sender to the request receiver allowing more than one receiver a chance to handle the request
How. TheRequest
represents the initial data, then intermediary results and finally the final result. TheRequestHandler
functional interface processes theRequest
partially or completely. TheChainOfResponsibility
composes theRequestHandler
s and implements theRequestHandler
functional interface to process theRequest
Example. ThecashRequestHandlerChain
=ChainOfResponsibility
composes theCashRequestHandler
=RequestHandler
and implements theRequestHandler
interface to process theCashRequest
=Request
. TheCashRequest
has the initial amount to represent with the set of notes. TheCashRequest
is used to handle intermediary results by representing the remaining amount and a set of already processed notes. TheCashRequest
finally represent the final result of the set of notes with the amount remainder if any -
Command (usage)
What. Command encapsulates an action with the request parameters as an function/object and decouples the request for an action from the actual action performer. Allows action/request queueing, logging and undoable operations
How. TheCommand
object stores the request parameters and delegates the request to theReceiver
which performs the action. TheInvoker
object uses theCommand
interface and provides request queueing, logging and undoable operation functionality
Example. ThecookStarter()
,cookMainCourse()
andcookDessert()
functions implement theOrder
=Command
=Receiver
interface and store the request arguments in a closure. TheWaiter
=Invoker
queues theOrder
s and serves theOrder
s by using theOrder
interface -
Interpreter (usage)
What. Given a language (DSL), define a language grammar with an interpreter that use the grammar to interpret the language sentences
How. Define anExpression
class hierarchy for eachTerminalExpression
andNonterminalExpression
symbol in the language. The abstract syntax tree (AST) of the language sentence is aComposite
ofExpression
s and is used to evaluate the sentence. TheTerminalExpression
interprets the expression directly. TheNonterminalExpression
has a container of childrenExpression
s and recursively interprets every childExpression
. Interpreter does not describe how to build an AST. The AST can be build with a parser
Example. TheConstant
=TerminalExpression
, theAdd
=NonterminalExpression
and theMul
=NonterminalExpression
implement theExpression
interface. TheInterpreter
implements theExpression
interpretation algorithm -
Iterator (usage) #
What. Iterator provides a way to access the elements of an aggregate object/container of components sequentially without exposing the underlying representation (data structure) of the aggregate. Iterator encapsulates the traversal algorithm of a given aggregate object/container of components
How. TheContainerIterator
implements theIterator
interface for theContainer
ofComponent
s to traverse sequentially theComponent
s of theContainer
without exposing the underlying aggregate representation. Client access theComponent
s of theContainer
only through theIterator
interface
Example. TheFruitsIterator
=ContainerIterator
implements theIterator
interface for theFruits
=Container
ofFruit
=Component
-
Mediator (usage)
What. Mediator defines an object that encapsulates how a set of other objects interact. Mediator promotes loose coupling between collaborators
How. TheColleague
s interact with each other through theMediator
interface without having any references to each other. TheMediator
object has references to everyColleague
and interacts with aColleague
through a commonColleague
interface
Example. TheAirplane
=Colleague
and theHelicopter
=Colleague
implement theAircraft
=Colleague
interface and accept theControlTower
=Mediator
. TheAirplane
and theHelicopter
communicate with each other through theControlTower
interface. TheControlTower
has the references to theAirplane
and theHelicopter
and forwards the messages through theAircraft
interface -
Memento (usage)
What. Memento captures and externalizes an object's internal state without violating encapsulation. Allows the object to be restored (undo/rollback) to the captured state later
How. TheCaretaker
requests theOriginator
to snapshotOriginator
's internal state into theMemento
object before using theOriginator
. To rollback theOriginator
's internal state theCaretaker
returns back theMemento
object to theOriginator
Example. TheCounter
=Originator
object provides aCounterMemento
=Memento
object to store to/retrieve from theCounter
internal state. The incremental count of theCounter
object could be altered by using theCounterMemento
object -
Observer (usage) #
What. Observer defines a one-to-many dependency between objects where a state change in one objectSubject
is automatically notified to all subjects' dependentsObservers
How. TheSubject
maintains a list of theObserver
s which implements theObserver
interface. When theSubject
's state changes theSubject
notifies all theObserver
s using theObserver
interface. TheSubject
and theObserver
s are loosely coupled as the state change notification is done through theObserver
interface
Example. TheBidder
=Observer
implements theBidObserver
=Observer
interface. TheAuctioneer
=Subject
implements theAuctioneerSubject
=Subject
interface. When the bid changes theAuctioneer
notifies all theBidders
through theBidObserver
interface -
State (usage)
What. State alters objects' behavior when object's state changes
How. TheState
interface implementation provides the request handling functionality and sets the nextState
.State
implements a state machine where each individual state is a derived class of theState
interface and each transition is defined in state interface method invocation
Example. TheVendingMachine
internal state goes through theShowProducts
,SelectProduct
,DepositMoney
andDeliverProduct
State
s by invoking theVendingMachine::proceed()
method which delegates to the currentState::handleRequest()
method which handles the request and sets the nextVendingMachine
State
-
Strategy (usage) #
What. Strategy defines a family of interchangeable at runtime algorithms and provides excellent support for the Open-Closed Principle
How. Define a set of interchangeable algorithms/strategies that implement theStrategy
interface. Based on the conditions at runtime dynamically select the appropriate algorithm/strategy. Client works only with theStrategy
interface
Example. TheTransportCompany
dynamically select the appropriate algorithm/strategygoByBus
orgoByTaxi
based on the size of the tourist group. BothgoByBus
andgoByTaxi
implements theTransport
=Strategy
interface under which the algorithms/strategies are provided to the client -
Template method (usage)
What. Template Method defines the skeleton of an algorithm (invariant) in one operation deferring some steps (variable) to subclasses (inversion of control) and preserving the overall structure of the algorithm
How. The abstract class defines the abstract algorithmTemplate
with the overall algorithm structure. The invariant algorithm steps are defined as final methods in the abstract class and the variant steps are open for overriding in theTemplate
specializations
Example. TheEmployee
=Template
defines the overall algorithm structure, the invariant and variant algorithm steps. TheDeveloper
and theArchitect
algorithm specialization override the variable algorithm steps -
Visitor (usage)
What. Visitor separates an algorithm from an object structure on which the algorithm operates. Visitor allows to add new operations through theVisitor
s to the existing object structure known asElement
s without modifying the structure
How. TheElement
interface defines a visitor operation for everyVisitor
type based on the abstractVisitor
interface implementing the dynamic dispatch on the abstractVisitor
interface. EveryVisitor
interface defines the overloaded for eachElement
type operation implementing the static dispatch on the concreteElement
type. EveryVisitor
interface implementation has the cross between the concreteElement
and the concreteVisitor
implementing the double dispatch (dynamic on theVisitor
type and static on theElement
type)
Example. TheXiaomiPhone
and theNokiaPhone
implement thePhone
=Element
interface for the dynamic dispatch on the abstractVisitor
type (one operation for eachVisitor
type). TheIntelWiFi
and theBroadcomWiFi
implement theWiFi
=Visitor
interface for the static dispatch on the concretePhone
type to switch on theWiFi
visitor operation. TheSonyCamera
and theSamsungCamera
implement theCamera
=Visitor
interface for the static dispatch on the concretePhone
type to take phones with theCamera
visitor operation. So the two visitor operations (switch on WiFi and take photo) are implemented on thePhone
Element
structure
- KISS - Keep It Simple, Stupid. Do the simplest thing that could possibly work. Avoid unnecessary complexity. Maintain balance between code simplicity and system flexibility, extensibility
- YAGNI - You Aren't Gonna Need It. Do not write code that is not necessary at the moment, but might be necessary in the future. Do the simplest thing that could possibly work
- DRY - Don't Repeat Yourself. Avoid duplication. Every piece of knowledge must have a single, unambiguous, authoritative representation within the system. The modification of any single element of the system should not require changes in other logically unrelated elements
- Abstraction principle. Function (interpretation) and structure
(representation) should be decoupled and independent. It should be possible to
use a value or a function without knowing how it is implemented. It should be
possible to change the implementation without breaking a client code
- Data abstraction (abstract data types, ADT). Hide a representation of a data type behind an interface (constructors, selectors, [mutators], type predicate)
- Procedure abstraction (higher-order functions, HOF). Define a generic algorithm in a function that takes as parameters other functions to perform specific tasks. Examples: map, filter, reduce, fold (fundamental iterator), unfold (fundamental constructor)
- Information hiding. Information should only be made available on the need-to-know basis. Locality principle scopes should be as small as possible. One piece of code that calls another piece of code should not know internals about that other piece of code. This make it possible to change internal parts of the called piece of code without being forced to change the calling piece of code accordingly. Expose as little as possible of the internal implementation details of a module to promote loose coupling between modules. Provide a stable interface to module functionality that will protect clients of a module from changes in the module implementation
- High cohesion. High cohesion is a degree to which the components inside a module belong together. A module has high cohesion when the module responsibility is clearly defined and the module has as few dependencies as possible. Single Responsibility Principle fosters high cohesion
- Loose coupling. Each module in the system has as little knowledge as possible about other modules in the system. Use interfaces to implement loose coupling between modules. High cohesion fosters loose coupling
- Robustness principle (flexible input, compliant output). Be conservative in what you produce (produce outputs compliant with a specification). Be tolerant in what you accept from others (validate and sanitize inputs as long as the meaning is clear)
- Top-down approach. Mostly used in Object-Oriented Programming (OOP) with interfaces and abstract classes. Decomposition of a system into compositional subsystems. An overview of the system is formulated specifying but not detailing any first-level subsystems. Each subsystem is then refined in yet greater detail, until the entire specification is reduced to base elements. Emphasizes complete understanding of a system and its subsystems. No coding can begin until a sufficient level of detail has been reached in the design phase
- Bottom-up approach. Mostly used in Functional Programming (FP) with function composition. Composition of basic elements together into a more complex components. The individual base elements of a system are first specified in great detail and immediately implemented. These elements are then linked together to form larger subsystems, until a complete top-level system is formed. Emphasizes coding and early testing, which can begin as soon as the first module has been specified. There is an uncertainty in how modules can be linked together to form a top-level system
- RAII - Resource Acquisition Is Initialization. Smart pointer: constructor acquires, destructor releases. Smart pointer is a scope-based resource management. When a resource gets out of scope via normal execution or thrown exception the resource is deallocated automatically by a smart pointer destructor. RAII only works for resources acquired and released by stack-allocated objects where there is a well-defined static object lifetime
- FCoI - Favor Composition + Delegation over Inheritance. Composition is a black box reuse through an interface and promotes loose coupling. Inheritance is white box reuse through inherited members/methods and implies tight coupling
- LoD - Law of Demeter. The principle of least knowledge/dependencies - don't talk to strangers, only talk to your immediate neighbors. LoD fosters loose coupling and information hiding
- ADP - Acyclic Dependency Principle. Circular dependencies should be avoided. Dependency Inversion Principle and creation of a new package with common components breaks circular dependencies
- SRP - Single Responsibility Principle. Software unit should have only one single and well-defined responsibility, only one reason to change. High cohesion fosters SRP
- OCP - Open-Closed Principle. Software unit should be open for extension (inheritance, Strategy, or Decorator design patterns), but closed for modification (interface with multiple polymorphic implementations)
- LSP - Liskov Substitution Principle. Hierarchy is used to build specialized types from a more general type. Polymorphism means that one single interface has multiple implementations. Subtype must be completely substitutable for its supertype. Preconditions cannot be strengthened in a subtype. Postconditions cannot be weakened in a subtype. Supertype invariants must be preserved in a subtype
- ISP - Interface Segregation Principle. Segregate one broad single interface into a set of smaller and highly cohesive interfaces, so other program components depend only on small cohesive interfaces instead of depending on a single broad interface, and other program components won't be required to implement all the functionality of a broad interface
- DIP - Dependency Inversion Principle. Avoid tight coupling between modules with the mediation of an abstraction layer (interface). Each module should depend on an abstraction (interface), not other modules directly. An abstraction (interface) provides behavior needed by a module through possibly multiple implementations. Common features should be consolidated in a shared abstractions exposed through interfaces. Dependency Inversion Principle fosters testability of components
- Make each program do one thing and do it well (quality components)
- To do a new job build afresh rather than complicate old programs by adding new features (Single Responsibility Principle, separation of concerns)
- Expect the output of every program to become the input to another program. Write programs to handle text streams, because text is a universal interface. Don't clutter the output with extraneous information. Don't insist for interactive input and allow for scripting (uniform communication)
- Favor composability (bottom-up approach in FP) over monolithic design
- Design and build software to be tried early (bottom-up approach in FP). Build prototype as soon as possible. Don't hesitate to throw away bad design and rebuild from scratch
- Use tools or even build tools to automate repetitive task (DevOps)
- Modularity. Write simple parts connected by clean interfaces (modularity + composability)
- Clarity. Clarity is better than cleverness
- Composition. Design programs to be connected to other programs (composability + uniform communication)
- Separation. Separate policies from mechanisms. Separate interfaces from engines (orthogonality)
- Simplicity. Design for simplicity. Add complexity only where you must
- Parsimony. Write a big program only when nothing else will do
- Transparency. Design for visibility to make issue resolution easier (Dependency Inversion Principle)
- Representation. Fold knowledge into data, so program logic can be simple and robust (first data structures and then algorithms)
- Silence. When a program has nothing surprising to say, it should say nothing
- Failure. When a program must fail, fail noisily as soon as possible
- Generation. Write programs to write programs when you can (metaprogramming and DSL)
- Optimization. Prototype before polishing. Get it working and correct before you optimize it to be fast
- Extensibility. Design for the future, because it will be here sooner than you think
- Codebase. One codebase tracked in revision control, many deployments
- Use revision control system (Git) to track changes to a codebase
- Set up one repository per app/service
- Single codebase is deployed into multiple environments (dev, test, staging, production) with different level of maturity/testing
- Dependencies. Explicitly declare and isolate dependencies
- Use a package manager (npm) to manage dependencies
- Always explicitly define dependency versions (package.json)
- Isolate locally installed dependencies in
~/.local/lib
from interference with system-wide packages in/usr/local/lib
- Only a language runtime and a package manager are required to run an app/service
- Explicitly include (into a Docker image) all system tools (curl) that an app/service depends on
- Configuration. Store configuration in each environment
- Keep strict separation of the environment-specific configuration from a codebase
- Do not store any credentials and secretes in a codebase
- Store environment-specific configuration in environment variables set up by IaC deployment scripts
- Backing services. Treat backing services as attached resources accessible
via URI
- Most of the apps/services use databases, queues, caches, email systems, cloud services (resources)
- App/service can easily swap backing service by changing a resource URI provided by an environment-specific configuration without any changes in a codebase
- Build, release, deploy. Strictly separate build and run stages
- A codebase is transformed into a deployment through the below stages (verified build => immutable release => automated deployment)
- Build stage transforms a source code (Git repository) into an executable artifact (Docker image). Fetch specific tag, download dependencies, compile an executable artifact, build a Docker image, test the application
- Release stage combines an environment-independent executable artifact with an environment-specific configuration into a deployment unit. Every release is immutable and should be uniquely tagged. Any change must create a new release
- Deployment stage launches an app/service in an environment
- Processes. Execute an app as one or more stateless, share-nothing
processes/containers
- Any data that needs to persist must be stored in a stateful database
- Process memory and a local file system can be used only as a temporary ephemeral storage
- Every app/service process should be idempotent
- Port binding. Export services via port binding + protocol
- App/service should be completely self-contained and does not rely on any server execution environment
- App/service should export HTTP/gRPC/AMQP as a service by binding to a port
- In production a reverse proxy (NGINX) routes requests from a public-facing host to a port-bound app/service
- Concurrency. Scale out via OS processes
- Use first-class, share-nothing Unix processes/daemons (horizontal scalability) for each type of workload (HTTP endpoints, background workers, databases)
- App/service should rely on OS process manager (systemd)
- Disposability. Maximize robustness with fast startup and graceful
shutdown
- App/service should be disposable: can be started or stopped quickly that facilitates fast elastic scalability
- App/service should minimize startup time and fail fast
- App/service should shut down gracefully on SIGTERM:
- For HTTP cease listening, let current request to finish, and then exit
- For AMPQ return current idempotent job to a queue, and then exit
- Dev/prod parity. Keep development, testing, staging, and production as
similar as possible
- App/service delivery pipeline shout be completely automated and use CI/CD
- Use the same Git repository, Docker image in all environments (dev, test, stage, prod)
- Logs. Treat logs as event streams
- Instrumentalize app/service with logs to get insights, telemetry, and observability
- Logs are a stream of time-ordered events collected in a centralized log aggregation system (Elasticsearch)
- App/serer should only emit all structured (JSON) logs to STDOUT
- Execution environment manages to augment and forward logs to a centralized log aggregation system (Elasticsearch) for introspection, real-time analysis, and alerts
- Admin processes. Run admin/management task as one-off processes in the
same codebase
- IaC admin source code and dependencies should ship with a app/service source code to avoid sync issues
- Information security in rest
- Confidentiality - ensure that only an authorized user has access to data
- Integrity - ensure that data is not altered by an unauthorized user
- Availability - ensure that data is available only to an authorized user
- Information security in transit. Alice sends a message to Bob
- Confidentiality - only Bob can read a message
- Integrity - Eve cannot alter a message
- Authenticity - only Alice could have sent a message
- Least privilege #. A subject should have the least amount of privilege (CPU/RAM quotas, file system/network access, data access permissions, business transaction limits, time-based constraints) explicitly granted to perform its business process with the objective to limit intentional or unintentional damage to data. The permission should be granted just before performing the operation and should be immediately revoked on a completion or failure of the operation. The function of the subject (not the identity) should control the assignment of permissions. Example: if a user only needs to read a file, then he should not be granted permission to write a file
- Secure by default: security baseline in terms of functionality. Delivered out-of-the-box functionality should be secure by default. A user might reduce security and increase risk if he is allowed to do so. Example: a user password expiration and complexity should be enabled by default. User might be allowed to weaken password requirements
- Fail-safe defaults: bottom line of user initial privileges. Prefer explicitly granted access over access exclusion. By default a user do not have access to any resource until access to a resource is granted explicitly, so on an operation failure the system security is not compromized
- Fail securely. When a system fails, it should fail to a state where security of a system is not compromised. Immediately release resources, decrease privileges, and maybe logout from an account on an operation failure
- Defense in depth #. Multiple security controls at each architectural level that approach risk from different perspectives are better as it makes much more difficult to exploit vulnerabilities. Example: secure coding + code reviews, explicit resource acquisition and privileges granting, security testing, input data validation/sanitization, secure deployment, proactive application monitoring and auditing, continuous security and risk assessment. Example: administrative web interface should be protected (a) by authentication, (b) only accessible from internal network, (c) with enabled audit logging
- Complete mediation #. Every access to every resource must be checked for authentication and authorization via system-wide central point of access control. Subsequent accesses to the same resource should also be checked and not cached. Performance (caching) vs security (explicit check of every request) trade off
- Separation of duties #. Do not grant multiple unrelated privileges to a single account. Require collaboration of multiple accounts to perform important operations securely in order to prevent intentional fraud or unintentional error. Example: application administrator should not be a super user of an application; an administrator should not be able to perform business operations on behalf of an application user
- Separation of privilege #. Every security control should be based on more than a single condition in order to remove a single point of failure. Example: when approving a request validate that (a) user is authenticated (b) user status is active (c) user is authorized to access the resource. Multi-factor authentication MFA (something you know, you have, you are)
- Economy of mechanism. Keep security simple. Avoid complex approaches to security controls as it is much easier to spot functional defects and security flaws in simple designs while it is very difficult to identify problems in complex designs. Complexity does not add security
- Open design. Avoid security by obscurity. Security of a system should not be dependent on secrecy of its design or implementation. Prefer well tested public security standards over homegrown hidden security controls. Keeping passwords in secret does not violate this principle as a password is not an algorithm. It is better to know a security level of standard security controls rather than do not know at all the security level of homegrown not extensively tested security controls. Examples: OAuth 2.0. Linux source code is publicly available, yet when properly secured, Linux is hard secure operating system
- Least common mechanism. Mechanisms to access a resource should not be shared between different subjects. Example: provide different login pages for different types of users; if one of the login pages is compromized, other login pages are not impacted
- Minimize attack surface area. Asses security risks introduced by a new feature, then adapt feature design and define security controls to minimize the attack surface area. Example: a search function of an online help feature may be vulnerable to an SQL injection attack; expose the feature only to authorized users, use data validation and escaping, or eliminate search function from the feature design by providing a better structured user interface to reduce the attack surface area
- Psychological acceptability. Security controls should not make resources more difficult to access than if security controls were not present. The more user friendly an interface is, the less likely a user will make a mistake when configuring and using a security control and expose a system to security breaches. Error messages should be descriptive and actionable but not convey unnecessary design details that may be used to compromise a system
- Do not trust services. Do not assume that third party partners have the same or higher security policies then yours. Put necessary security controls on third party services
- Fix security issues correctly. Once a security issue has been identified, (a) understand the root cause of it and determine the scope of it (b) develop a fix for it (c) implement required tests for it (d) add monitoring and auditing of it
- Weakest link #. A chain is only as strong as its weakest link. Focus on the weakest component in a system
- Zero trust security (zero trust [network] architecture, perimeterless
security). Never trust (no trust by default), always verify even inside a
network perimeter (traditional IT network security trusts anyone within a
network perimeter)
- Strict identity verification (MFA, mutual authentication, session expiration + re-authentication)
- Device verification (integrity check, device authentication)
- Microsegmentation for access segregation to prevent lateral movement
- Strict access control (roles, permissions, policies)
- Continuous monitoring inside and outside of a network perimeter
- SP provides secure flow control structures that allow for program proof and verification
- SP requires a single entry point and ideally single exit point, but multiple exist points are also allowed to simplify program code
- SP components
- Selection (conditionals, branching)
if/elif/else
,case/of/else
- Iteration (repetition, looping)
while
,for/in
- Code blocks (delimited sequence of statements)
code
- Subroutings (named and parametrized code block)
proc
,func
- Recursion (self-referencing)
- Selection (conditionals, branching)
- Deviations from SP
- Early exit from a function
return
- Early exit from a loop
continue
,break
- Exceptions
try/except
- Use unwind protection for automated resource management
try/finally
, RAII (stack-only) - Multiple entry points: coroutines, generators (yeild control / value + resume execution)
- Early exit from a function
- OOP - Object-Oriented Programming. OOP is like biological cells: messaging (uniform communication), state hiding (data abstraction), late binding (behavioral polymorphism). Object has well encapsulated structure (properties) and provides polymorphic behavior (methods) through a well defined interface (messages). Abstraction (hierarchy), separation of concerns (composability) and modularization (orthogonality) are the keys to master complexity
- Uniform metaphor. A system should be designed around a powerful metaphor that can be uniformly applied in all areas. Large applications are viewed in the same way as the fundamental units from which the system is built. Examples: lists/functions in Scheme, relations/queries in SQL, objects/messages in Smalltalk
- Good design. A system should be build with a minimum set of unchangeable parts. The unchangeable parts should be as general as possible. All parts of a system should be held in a uniform framework with a uniform interface
- Modulatiry. No component in a system should depend on the internal details of any other component. Component interdependencies in a system should be minimized
- Factoring via inheritance. Each component in a system should be defined only in one place. Use inheritance to provide well-factored design and avoid repetition (duplication). Inheritance propagates default behavior through increasingly more specific hierarchy of classes
- Classification. Group similar components into class hierarchy to reduce complexity. Class abstraction describes (a) message protocol that the object recognizes (explicit communication) (b) internal state change of the object (implicit communication). Object is a concrete instance of a class
- Objects. Use object-oriented model for storage. Objects are created by
sending a message to their classes. Objects are automatically disposed when
they are not needed any more and automatically reclaimed by a GC
- Protocol (explicit communication) is a set of messages that the object can respond to
- State (implicit communication) defines object-specific response to a message. All access to the internal state of an object is exclusively performed via message protocol (message-passing)
- Messages. Use message-oriented model for processing. Computing should be
intrinsic capability of objects that can be uniformly invoked by sending
messages. Message-passing metaphor decouples the intent (message) from actual
execution (method)
- Message = operation + arguments
- Polymorphism. A system should specify (via message protocol, interface) only behavior (interpretation) of objects, not their implementation (representation)
- Most programs are more complex than thay need to be because of bad design
- Data structures, and not algorithms, are central to programming
- Optimize only after the implementation when a bottleneck is identified
- Use consistent identificator names with minimum length, maximum information in a context
User story describes in an informal language a software feature from the end user perspective
As a <role, user, who>
I want <capability, goal, what>,
so that <benefit, reason, why>
Gherkin feature with scenarios specifies expected software behavior in a logical language that a user can understand
Feature: <feature name> (collection of scenarios)
<feature description>
Scenario: <scenario> (colleciton of steps)
Given: <preconditions setup>
When: <action trigger>
Then: <result verification>
Use case describes in text + diagram interaction between an actor (human or external system) and a system to achieve a goal
Title: <active verb one-liner>
Actor: <primary actor>
Goal: <goal in context>
Precondition: <initial system state>
Trigger: <actor action>
Success: <main success scenario>
Extensions: <alternative scenarios and enhancements>