Now
A PHILOSOPHY OF SOFTWARE DESIGN
is my
best friend
You can call it beautiful code when the code also makes it look like the language was made for the problem
Ward Cunningham
Photo by Shahadat Rahman on Unsplash
✅ Function names should be descriptive and consistent
✅ Output args should be avoided in place of return values
✅ Functions should be commands or queries but not both
Duplication may be the root of all evil in software
🤔
🤔 Don't repeat yourself - DRY
Programmer A sees duplication
Programmer A extracts duplication and gives it a name
Programmer A replaces the duplication with the new abstraction
Time passes
A new requirements appears for which the current abstraction is almost perfect
Programmer B gets tasked to implement this requirement
Programmer B feels honor-bound to retain the existing abstraction, but since it isn't exactly the same they have to alter the code to take a parameter.
Another new requirement arrives. Programmer X - another param, another conditional.
Repeat until code becomes unmaintainable
An ideal function has zero arguments
The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible.
😦
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
)
Side effects are lies. Your function promises to do one thing, but it also does other hidden things.
Zero args, but no side effects?
Sometimes it will make unexpected changes to the variables of its own class
😦
The first rule of functions is that they should be small
...every function in [Kent Beck's] program was just two, or three, or four lines long...
..that's how short your functions should be
@Throws(Exception::class)
fun testableHtml(pageData: PageData, includeSuiteSetup: Boolean): String? {
val wikiPage: WikiPage = pageData.getWikiPage()
val buffer = StringBuffer()
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
val suiteSetup: WikiPage? = PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage
)
if (suiteSetup != null) {
val pagePath: WikiPagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup)
val pagePathName: String = PathParser.render(pagePath)
buffer.append("!include -setup .").append(pagePathName).append("\n")
}
}
val setup: WikiPage? = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage)
if (setup != null) {
val setupPath: WikiPagePath = wikiPage.getPageCrawler().getFullPath(setup)
val setupPathName: String = PathParser.render(setupPath)
buffer.append("!include -setup .").append(setupPathName).append("\n")
}
}
buffer.append(pageData.getContent())
if (pageData.hasAttribute("Test")) {
val teardown: WikiPage? = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage)
if (teardown != null) {
val tearDownPath: WikiPagePath = wikiPage.getPageCrawler().getFullPath(teardown)
val tearDownPathName: String = PathParser.render(tearDownPath)
buffer.append("\n").append("!include -teardown .").append(tearDownPathName).append("\n")
}
// snip
}
pageData.setContent(buffer.toString())
return pageData.getHtml()
}
@Throws(Exception::class)
fun testableHtml(pageData: PageData, includeSuiteSetup: Boolean): String? {
val wikiPage: WikiPage = pageData.getWikiPage()
val buffer = StringBuffer()
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
val suiteSetup: WikiPage? = PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage
)
if (suiteSetup != null) {
val pagePath: WikiPagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup)
val pagePathName: String = PathParser.render(pagePath)
buffer.append("!include -setup .").append(pagePathName).append("\n")
}
}
val setup: WikiPage? = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage)
if (setup != null) {
val setupPath: WikiPagePath = wikiPage.getPageCrawler().getFullPath(setup)
val setupPathName: String = PathParser.render(setupPath)
buffer.append("!include -setup .").append(setupPathName).append("\n")
}
}
buffer.append(pageData.getContent())
if (pageData.hasAttribute("Test")) {
val teardown: WikiPage? = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage)
if (teardown != null) {
val tearDownPath: WikiPagePath = wikiPage.getPageCrawler().getFullPath(teardown)
val tearDownPathName: String = PathParser.render(tearDownPath)
buffer.append("\n").append("!include -teardown .").append(tearDownPathName).append("\n")
}
// snip
}
pageData.setContent(buffer.toString())
return pageData.getHtml()
}
class SetupTeardownIncluder private constructor(private val pageData: PageData) {
private lateinit var isSuite: Boolean
@Throws(Exception::class)
fun render(isSuite: Boolean): String? {
this.isSuite = isSuite
if (isTestPage()) includeSetupAndTeardownPages()
return pageData.getHtml()
}
@Throws(Exception::class)
private fun includeSetupAndTeardownPages() {
includeSetupPages()
includePageContent()
includeTeardownPages()
updatePageContent()
}
@Throws(Exception::class)
private fun includeSetupPages() {
if (isSuite) includeSuiteSetupPage()
includeSetupPage()
}
@Throws(Exception::class)
private fun includeSuiteSetupPage() {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup")
}
@Throws(Exception::class)
private fun includeSetupPage() {
include("SetUp", "-setup")
}
@Throws(Exception::class)
private fun updatePageContent() {
pageData.setContent(newPageContent.toString())
}
}
class SetupTeardownIncluder private constructor(
private val pageData: PageData
) {
@Throws(Exception::class)
fun render(isSuite: Boolean): String? {
this.isSuite = isSuite
if (isTestPage()) includeSetupAndTeardownPages()
return pageData.getHtml()
}
@Throws(Exception::class)
private fun includeSetupAndTeardownPages() {
includeSetupPages()
includePageContent()
includeTeardownPages()
updatePageContent()
}
@Throws(Exception::class)
private fun includeSetupPages() {
if (isSuite) includeSuiteSetupPage()
includeSetupPage()
}
@Throws(Exception::class)
private fun includeSuiteSetupPage() {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup")
}
@Throws(Exception::class)
private fun includeSetupPage() {
include("SetUp", "-setup")
}
@Throws(Exception::class)
private fun updatePageContent() {
pageData.setContent(newPageContent.toString())
}
}
Implied:
A clean functional decomposition uses very short niladic functions and side effects on members to summarize a problem in a way that approaches natural language
IEEE Technical Committee on Distributed Processing Outstanding Technical Contribution Award (2020)
Elected to National Academy of Engineering (2001)
ACM Grace Murray Hopper Award (1987)
Jeff Dean
Sanjay Ghemawat
Copyright Peter Adams Photography
[CS190 at Stanford] is taught in a fashion similar to a traditional English writing class. In an English class, students use an iterative process where they write a draft, get feedback, and then rewrite to make improvements.
In CS 190, students develop a substantial piece of software from scratch. We then go through extensive code reviews to identify design problems, and students revise their projects to fix the problems.
Photo by Aaron Burden
https://unsplash.com/@aaronburden
Photo by Cristian Palmer https://unsplash.com/@cristianpalmer
A software system is decomposed into a collection of modules that are relatively independent
Interface (cost: less is better)
Functionality (benefit: more is better)
Deep Module
Shallow Module
Another example of a deep module is the garbage collector in a language such as Go or Java. This module has no interface at all; it works invisibly behind the scenes to reclaim unused memory.
Photo by James Day www.jamesdayart.com
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com")
.build()
interface GitHub {
@GET("/repos/{owner}/{repo}/contributors")
suspend fun contributors(
@Path("owner") owner: String?,
@Path("repo") repo: String?
): Response<String>
}
val gitHubService = retrofit.create(GitHub::class.java)
Magic Frameworks are bad..."Magic" is when a framework does something automatically, in a cool way.
🤔
private fun addNullValueForAttribute(attribute: String) {
data[attribute] = null
}
No abstraction - all of the functionality is visible through its interface
No simpler to think about the interface than to think about the full implementation
More keystrokes to invoke the function than it would take for a caller to manipulate the data property directly
Adds complexity (in the form of a new interface for developers to learn) but provides no compensating benefit
class SetupTeardownIncluder private constructor(private val pageData: PageData) {
@Throws(Exception::class)
fun includeSetupAndTeardownPages() {
includeSetupPages() // these are not deep!
includePageContent()
includeTeardownPages()
updatePageContent()
}
@Throws(Exception::class)
private fun includeSetupPages() {
if (isSuite) includeSuiteSetupPage()
includeSetupPage()
}
@Throws(Exception::class)
private fun includeSuiteSetupPage() {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup")
}
@Throws(Exception::class)
private fun includeSetupPage() {
include("SetUp", "-setup")
}
@Throws(Exception::class)
private fun updatePageContent() {
pageData.setContent(newPageContent.toString())
}
}
@Throws(Exception::class)
fun includeSetupAndTeardownPages(isSuite: Boolean) {
if (isSuite) include(SuiteResponder.SUITE_SETUP_NAME, "-setup")
include("SetUp", "-setup")
newPageContent.append(pageData.getContent())
include("TearDown", "-teardown")
if (isSuite) include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown")
pageData.setContent(newPageContent.toString())
}
@Throws(Exception::class)
private fun include(pageName: String, arg: String) {
val inheritedPage = findInheritedPage(pageName)
if (inheritedPage != null) {
val pagePathName = getPathNameForPage(inheritedPage)
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n")
}
}
this is deep, let's keep it!
class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
collectFlows()
}
private fun collectFlows() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
collectViewStates()
}
launch {
collectEvents()
}
}
}
}
private suspend fun collectViewStates() {
viewModel.viewStates.collect {
binding.update(it)
}
}
private suspend fun collectEvents() {
viewModel.viewStates.collect {
handle(it)
}
}
}
class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.viewStates.collect {
binding.update(it)
}
}
launch {
viewModel.events.collect {
handle(it)
}
}
}
}
}
}
From Middle English abstract, borrowed from Latin abstractus, perfect passive participle of abstrahō (“draw away”), formed from abs- (“away”) + trahō (“to pull, draw”).
An abstraction is a simplified view of an entity, which omits unimportant details
An abstraction can go wrong in two ways:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
collectFlows()
}
Which flows?
When are they collected?
Omits important details
To sum up: If a function is only called from a single place, consider inlining it.
Besides awareness of the actual code being executed, inlining functions also has the benefit of not making it possible to call the function from other places.
http://www.softwareonthebrain.com/2022/01/the-misunderstood-single-responsibility.html
Photo by Nathan Dumlao on Unsplash
Single responsibility
Open/closed
Liskov substitution
Interface segregation
Dependency inversion
There should never be more than one reason for a class to change. In other words, every class should have only one responsibility.
The Single Responsibility Principle says that code should only do one thing. Any non-trivial code can have any number of reasons to change, which may or may not include the one you had in mind.
interface Call {
fun request(): Request
fun enqueue(responseCallback: Callback)
fun cancel()
fun isExecuted(): Boolean
fun isCanceled(): Boolean
fun clone(): Call
}
interface Call {
fun request(): Request
fun enqueue(responseCallback: Callback)
fun clone(): Call
fun executionManager(): ExecutionManager
}
interface ExecutionManager { // WTF?
fun cancel()
fun isExecuted(): Boolean
fun isCanceled(): Boolean
}
Should we split this class to make for a "single reason to change"?
The Misunderstood Single Responsibility Principle
I recently read Adaptive Code which, as of today, has a 4.7/5 rating on Amazon with 123 ratings. There is a whole chapter on the SRP. And throughout, it incorrectly explains the SRP as relating to classes doing too many things and justifies design decisions based on that alone.
Often people who like tiny classes will use it to justify making many more even tinier classes.
What starts as a simple, easy-to-understand class ends up being broken into many abstractions that are now harder to understand collectively.
When deciding whether to combine or separate, the goal is to reduce the complexity of the system as a whole and improve its modularity.
It might appear that the best way to achieve the goal is to divide the system into a large number of small components: the smaller the components, the simpler each individual component is likely to be.
for example, methods that were together in a single class before subdivision may be in different classes after subdivision, and possibly in different files.
if there are dependencies between the components, then separation is bad: developers will end up flipping back and forth between the components.
Photo by Pablo Varela on Unsplash
The principle actually means that the code in a given module has to be owned by one and only one ultimate business owner (or functional unit). If that isn't true you have to break it up.
Joe Lynch
CFO and COO both depend on the code that calculates employee hours. Calculating hours is simple math that everyone should agree on, right?
⏰
One executive requests a change to that code and it breaks the other's business rule. The hours calculation cannot be shared: that piece of code should have a Finance version and an Operations version, despite the fact that that violates our DRY and cohesion sensibilities.
Photo by Matt Walsh on Unsplash
1. Bring together if information is shared
chat_layout.xml
ChatViewModel.kt
Slides inspired by Leland Richardson
1. Bring together if information is shared
ChatScreen.kt
ChatViewModel.kt
Slides inspired by Leland Richardson
2. Bring together if it will simplify the interface
3. Bring together to eliminate duplication
4. Separate general-purpose and special-purpose code
If a module contains a mechanism that can be used for several different purposes, then it should provide just that one general-purpose mechanism.
It should not include code that specializes the mechanism for a particular use, nor should it contain other general purpose-mechanisms.
class Node {
val children = mutableListOf<Node>()
}
class NodeApplier(node: Node) : AbstractApplier<Node>(node) {
override fun onClear() {}
override fun insertBottomUp(index: Int, instance: Node) {}
override fun insertTopDown(index: Int, instance: Node) {
current.children.add(index, instance)
}
override fun move(from: Int, to: Int, count: Int) {
current.children.move(from, to, count)
}
override fun remove(index: Int, count: Int) {
current.children.remove(index, count)
}
}
Hypothetical Compose monolith
node management
Material UI
Mixing node management with UI precludes other uses like Molecule
Not seeing Material UI as a special case of UI precludes other UI
Photo by Jukan Tateisi on Unsplash
@d_rawers
@drawers@androiddev.social